Compare commits

...

33 Commits

Author SHA1 Message Date
Hassan Ben Jobrane
ef125216bb Merge pull request #2242 from nhost/changeset-release/main
chore: update versions
2023-09-15 17:33:37 +01:00
github-actions[bot]
fb43fefb5c chore: update versions 2023-09-15 16:22:32 +00:00
Hassan Ben Jobrane
73744c90f0 Merge pull request #2241 from nhost/feat/node18-announcement
feat: add node18 announcement banner
2023-09-15 17:19:27 +01:00
Hassan Ben Jobrane
9fbea9787e chore: add changeset 2023-09-15 16:47:21 +01:00
Hassan Ben Jobrane
e5f54bc197 feat: add node18 announcement banner 2023-09-15 16:43:59 +01:00
Hassan Ben Jobrane
10a6ae4853 Merge pull request #2233 from nhost/changeset-release/main
chore: update versions
2023-09-13 17:11:54 +01:00
github-actions[bot]
d6ca1c7cfd chore: update versions 2023-09-13 13:59:01 +00:00
Hassan Ben Jobrane
bb85a95eda Merge pull request #2236 from nhost/fix/run/subdomain-optional
fix: run: handle subdomain nullability
2023-09-13 14:56:22 +01:00
Hassan Ben Jobrane
e84acf4692 chore: add changeset 2023-09-13 13:06:05 +01:00
Hassan Ben Jobrane
2f20a70a28 fix(run): subdomain is not set when creating a new service 2023-09-13 13:00:18 +01:00
David Barroso
819e1e97dc chore (docs): update fqdn format for nhost run (#2232) 2023-09-12 14:54:39 +02:00
Hassan Ben Jobrane
7c1cca0a43 Merge pull request #2231 from nhost/changeset-release/main
chore: update versions
2023-09-12 13:09:07 +01:00
github-actions[bot]
0f51f4e868 chore: update versions 2023-09-12 12:05:55 +00:00
Hassan Ben Jobrane
97a6fcead9 Merge pull request #2230 from nhost/feat/run/copy-urls-dialog
feat(run): add dialog to copy service urls
2023-09-12 13:03:17 +01:00
Hassan Ben Jobrane
b7c799d62c chore: add changeset 2023-09-12 12:04:41 +01:00
Hassan Ben Jobrane
18b14b27fd refactor: pass service data directly to the details dialog 2023-09-12 12:01:20 +01:00
Hassan Ben Jobrane
67a867c93a feat: add dialog to copy service urls 2023-09-11 19:52:51 +01:00
David Barroso
0a1fb12467 feat: observability: add egress/requests metrics to general dashboard (#2227) 2023-09-06 18:03:25 +02:00
Hassan Ben Jobrane
78467ee348 Merge pull request #2219 from nhost/changeset-release/main
chore: update versions
2023-09-04 11:58:34 +01:00
github-actions[bot]
c24eef0db9 chore: update versions 2023-09-04 10:24:35 +00:00
Hassan Ben Jobrane
2159b8171e Merge pull request #2218 from nhost/fix/format-functions-execution
fix: dashboard: usage stats
2023-09-04 11:21:25 +01:00
Hassan Ben Jobrane
8903e6abd9 chore: add changeset 2023-09-02 15:18:54 +01:00
Hassan Ben Jobrane
7290260990 fix: show correct egress volume limit 2023-09-02 15:15:43 +01:00
Hassan Ben Jobrane
06529a1ea4 fix: round up functions duration 2023-09-02 15:14:29 +01:00
Hassan Ben Jobrane
607d89e2aa Merge pull request #2215 from nhost/changeset-release/main
chore: update versions
2023-09-01 19:13:41 +01:00
github-actions[bot]
0cca72311c chore: update versions 2023-09-01 15:26:44 +00:00
Hassan Ben Jobrane
a6525b6467 Merge pull request #2214 from nhost/feat/update-usage-metrics
feat(dashboard): update usage metrics
2023-09-01 16:24:06 +01:00
Hassan Ben Jobrane
387be37b6e chore: remove redundant egress card 2023-09-01 15:30:01 +01:00
Hassan Ben Jobrane
c8fd8bbcc7 fix: update storage upper limit for pro plan 2023-09-01 13:01:27 +01:00
Hassan Ben Jobrane
bfb34bad00 fix: use correct value for functions duration 2023-09-01 12:28:19 +01:00
Hassan Ben Jobrane
666a75a233 chore: add changeset 2023-09-01 12:26:41 +01:00
Hassan Ben Jobrane
3b050217df feat(dashboard): tweak usage metrics 2023-09-01 12:25:15 +01:00
Hassan Ben Jobrane
0ed4481615 feat(dashboard): update usage metrics 2023-09-01 11:14:49 +01:00
20 changed files with 1643 additions and 275 deletions

View File

@@ -1,5 +1,35 @@
# @nhost/dashboard
## 0.20.14
### Patch Changes
- 9fbea9787: feat: add node18 announcement
## 0.20.13
### Patch Changes
- e84acf469: fix(run): handle subdomain undefined error when creating a new service
## 0.20.12
### Patch Changes
- b7c799d62: feat(run): add dialog to copy registry and URLs
## 0.20.11
### Patch Changes
- 8903e6abd: fix(dashboard): show correct egress limit in usage stats
## 0.20.10
### Patch Changes
- 666a75a23: feat(dashboard): add functions execution time and egress volume to usage stats
## 0.20.9
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "0.20.9",
"version": "0.20.14",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",

View File

@@ -37,10 +37,10 @@ export interface AnnouncementContextProps {
// Note: You can define the active announcement here.
const announcement: AnnouncementType = {
id: 'nhost-run',
href: 'https://discord.com/invite/9V7Qb2U',
id: 'node-18',
href: 'https://github.com/nhost/nhost/discussions/2239',
content:
'Now you can bring custom and third-party OSS services to run alongside your Nhost projects',
"Starting October 1st, we're upgrading to Node.js 18 for improved performance, security, and stability. Learn more.",
};
export const AnnouncementContext = createContext<AnnouncementContextProps>({});

View File

@@ -7,14 +7,15 @@ import MaterialLinearProgress, {
export interface LinearProgressProps extends MaterialLinearProgressProps {}
const LinearProgress = styled(MaterialLinearProgress)(({ theme }) => ({
const LinearProgress = styled(MaterialLinearProgress)(({ theme, value }) => ({
height: 12,
borderRadius: 1,
[`&.${linearProgressClasses.colorPrimary}`]: {
backgroundColor: theme.palette.grey[300],
},
[`& .${linearProgressClasses.bar}`]: {
backgroundColor: theme.palette.primary.main,
backgroundColor:
value >= 100 ? theme.palette.error.dark : theme.palette.primary.main,
},
}));

View File

@@ -41,11 +41,6 @@ export default function OverviewMetrics() {
numberOfDecimals: 0,
}),
},
{
label: 'Egress Volume',
tooltip: 'Amount of data your services have sent to users',
value: prettifySize(data?.egressVolume?.value || 0),
},
{
label: 'Logs',
tooltip: 'Amount of logs stored',

View File

@@ -96,7 +96,7 @@ export function OverviewUsageMetrics() {
remoteAppMetricsData?.filesAggregate?.aggregate?.sum?.size || 0;
const totalStorage = currentProject?.plan?.isFree
? 1 * 1000 ** 3 // 1 GB
: 10 * 1000 ** 3; // 10 GB
: 50 * 1000 ** 3; // 10 GB
// metrics for users
const usedUsers = remoteAppMetricsData?.usersAggregate?.aggregate?.count || 0;
@@ -105,6 +105,16 @@ export function OverviewUsageMetrics() {
// metrics for functions
const usedFunctions = functionsInfoData?.app.metadataFunctions.length || 0;
const totalFunctions = currentProject?.plan?.isFree ? 10 : 50;
const usedFunctionsDuration = projectMetrics?.functionsDuration.value || 0;
const totalFunctionsDuration = currentProject?.plan?.isFree
? 3600 // 1 hour
: 3600 * 10; // 10 hours
// metrics for egress
const usedEgressVolume = projectMetrics?.egressVolume.value || 0;
const totalEgressVolume = currentProject?.plan?.isFree
? 5 * 1000 ** 3 // 5 GB
: 50 * 1000 ** 3; // 50 GB
if (metricsLoading) {
return (
@@ -112,7 +122,9 @@ export function OverviewUsageMetrics() {
<UsageProgress label="Database" percentage={0} />
<UsageProgress label="Storage" percentage={0} />
<UsageProgress label="Users" percentage={0} />
<UsageProgress label="Functions" percentage={0} />
<UsageProgress label="Number of Functions" percentage={0} />
<UsageProgress label="Functions Execution Time" percentage={0} />
<UsageProgress label="Egress Volume" percentage={0} />
</div>
);
}
@@ -139,6 +151,18 @@ export function OverviewUsageMetrics() {
used={usedFunctions}
percentage={100}
/>
<UsageProgress
label="Functions"
used={usedFunctionsDuration}
percentage={100}
/>
<UsageProgress
label="Egress"
used={usedEgressVolume}
percentage={100}
/>
</div>
);
}
@@ -167,11 +191,25 @@ export function OverviewUsageMetrics() {
/>
<UsageProgress
label="Functions"
label="Number of Functions"
used={usedFunctions}
total={totalFunctions}
percentage={(usedFunctions / totalFunctions) * 100}
/>
<UsageProgress
label="Functions Execution Time"
used={Math.trunc(usedFunctionsDuration)}
total={`${totalFunctionsDuration} seconds`}
percentage={(usedFunctionsDuration / totalFunctionsDuration) * 100}
/>
<UsageProgress
label="Egress Volume"
used={prettifySize(usedEgressVolume)}
total={prettifySize(totalEgressVolume)}
percentage={(usedEgressVolume / totalEgressVolume) * 100}
/>
</div>
);
}

View File

@@ -43,6 +43,7 @@ import { toast } from 'react-hot-toast';
import { parse } from 'shell-quote';
import * as Yup from 'yup';
import { ServiceConfirmationDialog } from './components/ServiceConfirmationDialog';
import { ServiceDetailsDialog } from './components/ServiceDetailsDialog';
export enum PortTypes {
HTTP = 'http',
@@ -94,7 +95,7 @@ export interface ServiceFormProps extends DialogFormProps {
/**
* if there is initialData then it's an update operation
*/
initialData?: ServiceFormValues;
initialData?: ServiceFormValues & { subdomain?: string }; // subdomain is only set on the backend
/**
* Function to be called when the operation is cancelled.
@@ -119,6 +120,10 @@ export default function ServiceForm({
const { currentProject } = useCurrentWorkspaceAndProject();
const [insertRunServiceConfig] = useInsertRunServiceConfigMutation();
const [replaceRunServiceConfig] = useReplaceRunServiceConfigMutation();
const [detailsServiceId, setDetailsServiceId] = useState('');
const [detailsServiceSubdomain, setDetailsServiceSubdomain] = useState(
initialData?.subdomain,
);
const [createServiceFormError, setCreateServiceFormError] =
useState<Error | null>(null);
@@ -196,11 +201,13 @@ export default function ServiceForm({
config,
},
});
setDetailsServiceId(serviceID);
} else {
// Insert service config
const {
data: {
insertRunService: { id: newServiceID },
insertRunService: { id: newServiceID, subdomain },
},
} = await insertRunService({
variables: {
@@ -227,6 +234,9 @@ export default function ServiceForm({
},
},
});
setDetailsServiceId(newServiceID);
setDetailsServiceSubdomain(subdomain);
}
};
@@ -254,8 +264,6 @@ export default function ServiceForm({
getToastStyleProps(),
);
// await refetchWorkspaceAndProject();
// refestch the services
onSubmit?.();
} catch {
// Note: The toast will handle the error.
@@ -277,6 +285,29 @@ export default function ServiceForm({
});
};
useEffect(() => {
(async () => {
if (detailsServiceId) {
openDialog({
title: 'Service Details',
component: (
<ServiceDetailsDialog
serviceID={detailsServiceId}
image={formValues.image}
subdomain={detailsServiceSubdomain}
ports={formValues.ports}
/>
),
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
})();
}, [detailsServiceId, detailsServiceSubdomain, formValues, openDialog]);
const pricingExplanation = () => {
const vCPUs = `${formValues.compute.cpu / RESOURCE_VCPU_MULTIPLIER} vCPUs`;
const mem = `${formValues.compute.memory} MiB Mem`;
@@ -314,7 +345,7 @@ export default function ServiceForm({
<Tooltip title="Name of the service, must be unique per project.">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
className="w-4 h-4"
color="primary"
/>
</Tooltip>
@@ -354,7 +385,7 @@ export default function ServiceForm({
>
<InfoIcon
aria-label="Info"
className="h-4 w-4"
className="w-4 h-4"
color="primary"
/>
</Tooltip>
@@ -385,7 +416,7 @@ export default function ServiceForm({
<Tooltip title="Command to run when to start the service. This is optional as the image may already have a baked-in command.">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
className="w-4 h-4"
color="primary"
/>
</Tooltip>
@@ -429,7 +460,7 @@ export default function ServiceForm({
{createServiceFormError && (
<Alert
severity="error"
className="grid grid-flow-col items-center justify-between px-4 py-3"
className="grid items-center justify-between grid-flow-col px-4 py-3"
>
<span className="text-left">
<strong>Error:</strong> {createServiceFormError.message}

View File

@@ -32,20 +32,20 @@ export default function PortsFormSection() {
name: 'ports',
});
const formValues = useWatch<ServiceFormValues>();
const formValues = useWatch<ServiceFormValues & { subdomain: string }>();
const onChangePortType = (value: string | undefined, index: number) =>
setValue(`ports.${index}.type`, value as PortTypes);
const showURL = (index: number) =>
formValues.subdomain &&
formValues.ports[index]?.type === PortTypes.HTTP &&
formValues.ports[index]?.publish;
const getPortURL = (_port: string | number, _name: string) => {
const getPortURL = (_port: string | number, subdomain: string) => {
const port = Number(_port) > 0 ? Number(_port) : '[port]';
const name = _name && _name.length > 0 ? _name : '[name]';
return `https://${currentProject?.subdomain}-${name}-${port}.svc.${currentProject?.region.awsName}.${currentProject?.region.domain}`;
return `https://${subdomain}-${port}.svc.${currentProject?.region.awsName}.${currentProject?.region.domain}`;
};
return (
@@ -144,7 +144,7 @@ export default function PortsFormSection() {
title="URL"
value={getPortURL(
formValues.ports[index]?.port,
formValues.name,
formValues.subdomain,
)}
/>
)}

View File

@@ -0,0 +1,84 @@
import { useDialog } from '@/components/common/DialogProvider';
import { Button } from '@/components/ui/v2/Button';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { InfoCard } from '@/features/projects/overview/components/InfoCard';
import type { ConfigRunServicePort } from '@/utils/__generated__/graphql';
export interface ServiceDetailsDialogProps {
/**
* The id of the service
*/
serviceID: string;
/**
* The subdomain of the service
*/
subdomain: string;
/**
* The image of the service
*/
image: string;
/**
* The image of the service
* We use partial here because `port` is set as required in ConfigRunServicePort
*/
ports: Partial<ConfigRunServicePort>[];
}
export default function ServiceDetailsDialog({
serviceID,
subdomain,
image,
ports,
}: ServiceDetailsDialogProps) {
const { currentProject } = useCurrentWorkspaceAndProject();
const { closeDialog } = useDialog();
const getPortURL = (_port: string | number) => {
const port = Number(_port) > 0 ? Number(_port) : '[port]';
return `https://${subdomain}-${port}.svc.${currentProject?.region.awsName}.${currentProject?.region.domain}`;
};
return (
<div className="flex flex-col gap-4 px-6 pb-6">
<div className="flex flex-col gap-2">
<Text color="secondary">Private registry</Text>
<InfoCard
title=""
value={
image ||
`registry.${currentProject.region.awsName}.${currentProject.region.domain}/${serviceID}`
}
/>
</div>
{ports?.length > 0 && (
<div className="flex flex-col gap-2">
<Text color="secondary">Ports</Text>
{ports
.filter((port) => port.publish)
.map((port) => (
<InfoCard
title={`${port.type}:${port.port}`}
value={getPortURL(port.port)}
/>
))}
</div>
)}
<Button
className="w-full"
color="primary"
onClick={() => closeDialog()}
autoFocus
>
OK
</Button>
</div>
);
}

View File

@@ -0,0 +1,2 @@
export * from './ServiceDetailsDialog';
export { default as ServiceDetailsDialog } from './ServiceDetailsDialog';

View File

@@ -76,6 +76,7 @@ export default function ServicesList({
initialData={{
...service.config,
image: service.config?.image?.image,
subdomain: service.subdomain,
command: service.config?.command?.join(' '),
ports: service.config?.ports?.map((item) => ({
port: item.port,

View File

@@ -17,6 +17,9 @@ query GetProjectMetrics(
) {
value
}
functionsDuration: getFunctionsDuration(appID: $appId, from: $from, to: $to) {
value
}
postgresVolumeCapacity: getPostgresVolumeCapacity(appID: $appId) {
value
}

View File

@@ -1,6 +1,7 @@
query getRunService($id: uuid!, $resolve: Boolean!) {
runService(id: $id) {
id
subdomain
config(resolve: $resolve) {
name
image {

View File

@@ -9,6 +9,7 @@ query getRunServices(
id
createdAt
updatedAt
subdomain
config(resolve: $resolve) {
name
image {

View File

@@ -1,6 +1,6 @@
mutation insertRunService($object: run_service_insert_input!) {
insertRunService(object: $object) {
id
appID
subdomain
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,11 @@
# @nhost/docs
## 0.5.1
### Patch Changes
- 819e1e97d: update fqdn format for nhost run
## 0.5.0
### Minor Changes

View File

@@ -112,8 +112,8 @@ Currently, only services of type `http` can be exposed to the internet.
2. Once the service of type `http` is published, you can connect to it using a URL with the following format:
`https://<subdomain>-<svc_name>-<port>.svc.<region>.nhost.run`
`https://<run_service_subdomain>-<port>.svc.<region>.nhost.run`
For example:
`https://zlbmqjfczuwqvsquujno-mysvc-3000.svc.eu-central-1.nhost.run`
`https://zlbmqjfczuwqvsquujno-3000.svc.eu-central-1.nhost.run`

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/docs",
"version": "0.5.0",
"version": "0.5.1",
"private": true,
"scripts": {
"docusaurus": "docusaurus",

View File

@@ -726,13 +726,226 @@
"title": "Service Restarts",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "bytes"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 37
},
"id": 39,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "sum by(ingress) (irate(nginx_ingress_controller_response_size_sum[$__rate_interval]))",
"interval": "2m",
"legendFormat": "__auto",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "sum(irate(fastly_prom_exporter_bytes_sent[$__rate_interval]))",
"hide": false,
"interval": "2m",
"legendFormat": "storage-cdn",
"range": true,
"refId": "B"
}
],
"title": "Egress Traffic",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 37
},
"id": 41,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "builder",
"expr": "sum by(ingress) (irate(nginx_ingress_controller_requests[$__interval]))",
"interval": "2m",
"legendFormat": "__auto",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "builder",
"expr": "sum(irate(fastly_prom_exporter_requests_total[$__interval]))",
"hide": false,
"interval": "2m",
"legendFormat": "storage-cdn",
"range": true,
"refId": "B"
}
],
"title": "Requests Rate",
"type": "timeseries"
},
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 37
"y": 45
},
"id": 10,
"panels": [],
@@ -801,7 +1014,7 @@
"h": 8,
"w": 12,
"x": 0,
"y": 38
"y": 46
},
"id": 12,
"options": {
@@ -908,7 +1121,7 @@
"h": 8,
"w": 12,
"x": 12,
"y": 38
"y": 46
},
"id": 14,
"options": {
@@ -1015,7 +1228,7 @@
"h": 8,
"w": 12,
"x": 0,
"y": 46
"y": 54
},
"id": 16,
"options": {
@@ -1120,7 +1333,7 @@
"h": 8,
"w": 12,
"x": 12,
"y": 46
"y": 54
},
"id": 19,
"options": {
@@ -1171,7 +1384,7 @@
"h": 1,
"w": 24,
"x": 0,
"y": 54
"y": 62
},
"id": 21,
"panels": [],
@@ -1240,7 +1453,7 @@
"h": 8,
"w": 12,
"x": 0,
"y": 55
"y": 63
},
"id": 22,
"options": {
@@ -1347,7 +1560,7 @@
"h": 8,
"w": 12,
"x": 12,
"y": 55
"y": 63
},
"id": 23,
"options": {
@@ -1397,7 +1610,7 @@
"h": 1,
"w": 24,
"x": 0,
"y": 63
"y": 71
},
"id": 25,
"panels": [],
@@ -1466,7 +1679,7 @@
"h": 8,
"w": 12,
"x": 0,
"y": 64
"y": 72
},
"id": 26,
"options": {
@@ -1573,7 +1786,7 @@
"h": 8,
"w": 12,
"x": 12,
"y": 64
"y": 72
},
"id": 27,
"options": {
@@ -1623,7 +1836,7 @@
"h": 1,
"w": 24,
"x": 0,
"y": 72
"y": 80
},
"id": 29,
"panels": [],
@@ -1692,7 +1905,7 @@
"h": 8,
"w": 12,
"x": 0,
"y": 73
"y": 81
},
"id": 30,
"options": {
@@ -1799,7 +2012,7 @@
"h": 8,
"w": 12,
"x": 12,
"y": 73
"y": 81
},
"id": 31,
"options": {