Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@babel/preset-typescript": "^7.18.6",
"@cloudscape-design/board-components": "^3.0.0",
"@cloudscape-design/browser-test-tools": "^3.0.0",
"@cloudscape-design/chart-components": "^1.0.0",
"@cloudscape-design/chat-components": "^1.0.0",
"@cloudscape-design/code-view": "^3.0.0",
"@cloudscape-design/collection-hooks": "^1.0.0",
Expand Down Expand Up @@ -61,6 +62,7 @@
"eslint-plugin-react": "^7.30.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"highcharts": "^12.2.0",
"husky": "^8.0.1",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
Expand Down
114 changes: 69 additions & 45 deletions src/pages/dashboard/widgets/chart-commons.tsx
Original file line number Diff line number Diff line change
@@ -1,73 +1,97 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0
import React from 'react';
import React, { useEffect, useState } from 'react';
import type Highcharts from 'highcharts';

import { CartesianChartProps } from '@cloudscape-design/chart-components';
import Box from '@cloudscape-design/components/box';

export const percentageFormatter = (value: number) => `${(value * 100).toFixed(0)}%`;

const numberTickFormatter = (value: number) => {
export const numberTickFormatter = (value: number | null) => {
if (value === null) {
return '';
}
if (Math.abs(value) < 1000) {
return value.toString();
}
return (value / 1000).toFixed() + 'k';
};

export const dateTimeFormatter = (date: Date) =>
export const dateTimeFormatter = (date: number | null) =>
date
.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
hour12: false,
})
.split(',')
.join('\n');
? new Date(date)
.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
hour12: false,
})
.split(',')
.join('\n')
: '';

export const dateFormatter = (date: Date) =>
export const dateFormatter = (date: number | null) =>
date
.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
hour12: false,
})
.split(' ')
.join('\n');
? new Date(date)
.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
hour12: false,
})
.split(' ')
.join('\n')
: '';

export const commonChartProps = {
loadingText: 'Loading chart',
errorText: 'Error loading data.',
recoveryText: 'Retry',
empty: (
<Box textAlign="center" color="inherit">
<b>No data available</b>
<Box variant="p" color="inherit">
There is no data available
export const commonChartProps: Omit<CartesianChartProps, 'highcharts' | 'series'> = {
noData: {
statusType: 'finished',
empty: (
<Box textAlign="center" color="inherit">
<b>No data available</b>
<Box variant="p" color="inherit">
There is no data available
</Box>
</Box>
</Box>
),
noMatch: (
<Box textAlign="center" color="inherit">
<b>No matching data</b>
<Box variant="p" color="inherit">
There is no matching data to display
),
noMatch: (
<Box textAlign="center" color="inherit">
<b>No matching data</b>
<Box variant="p" color="inherit">
There is no matching data to display
</Box>
</Box>
</Box>
),
),
},
i18nStrings: {
filterLabel: 'Filter displayed data',
filterPlaceholder: 'Filter data',
filterSelectedAriaLabel: 'selected',
loadingText: 'Loading chart',
errorText: 'Error loading data.',
recoveryText: 'Retry',
seriesFilterLabel: 'Filter displayed data',
seriesFilterPlaceholder: 'Filter data',
seriesFilterSelectedAriaLabel: 'selected',
legendAriaLabel: 'Legend',
chartAriaRoleDescription: 'line chart',
xAxisAriaRoleDescription: 'x axis',
yAxisAriaRoleDescription: 'y axis',
yTickFormatter: numberTickFormatter,
xAxisRoleDescription: 'x axis',
yAxisRoleDescription: 'y axis',
},
};

export const lineChartInstructions =
'Use up/down arrow keys to navigate between series, and left/right arrow keys to navigate within a series.';

export const barChartInstructions = 'Use left/right arrow keys to navigate between data groups.';

export const useHighcharts = () => {
const [highcharts, setHighcharts] = useState<typeof Highcharts | null>(null);
useEffect(() => {
const load = async () => {
const importedHighcharts = await import('highcharts');
await import('highcharts/modules/accessibility');
await import('highcharts/highcharts-more');
setHighcharts(importedHighcharts);
};
load();
}, []);
return highcharts;
};
64 changes: 30 additions & 34 deletions src/pages/dashboard/widgets/instance-hours/data.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,42 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0
import { BarChartProps } from '@cloudscape-design/components/bar-chart';

const cpuData = [
{ date: new Date(2020, 8, 16), 'm1.large': 878, 'm1.xlarge': 491, 'm1.medium': 284, 'm1.small': 70 },
{ date: new Date(2020, 8, 17), 'm1.large': 781, 'm1.xlarge': 435, 'm1.medium': 242, 'm1.small': 96 },
{ date: new Date(2020, 8, 18), 'm1.large': 788, 'm1.xlarge': 478, 'm1.medium': 311, 'm1.small': 79 },
{ date: new Date(2020, 8, 19), 'm1.large': 729, 'm1.xlarge': 558, 'm1.medium': 298, 'm1.small': 97 },
{ date: new Date(2020, 8, 20), 'm1.large': 988, 'm1.xlarge': 530, 'm1.medium': 255, 'm1.small': 97 },
{ date: new Date(2020, 8, 21), 'm1.large': 1016, 'm1.xlarge': 445, 'm1.medium': 339, 'm1.small': 70 },
{ date: new Date(2020, 8, 22), 'm1.large': 987, 'm1.xlarge': 549, 'm1.medium': 273, 'm1.small': 62 },
{ date: new Date(2020, 8, 23), 'm1.large': 986, 'm1.xlarge': 518, 'm1.medium': 341, 'm1.small': 67 },
{ date: new Date(2020, 8, 24), 'm1.large': 925, 'm1.xlarge': 454, 'm1.medium': 382, 'm1.small': 68 },
{ date: new Date(2020, 8, 25), 'm1.large': 742, 'm1.xlarge': 538, 'm1.medium': 361, 'm1.small': 70 },
{ date: new Date(2020, 8, 26), 'm1.large': 920, 'm1.xlarge': 486, 'm1.medium': 262, 'm1.small': 91 },
{ date: new Date(2020, 8, 27), 'm1.large': 826, 'm1.xlarge': 457, 'm1.medium': 248, 'm1.small': 76 },
{ date: new Date(2020, 8, 28), 'm1.large': 698, 'm1.xlarge': 534, 'm1.medium': 243, 'm1.small': 66 },
{ date: new Date(2020, 8, 29), 'm1.large': 1003, 'm1.xlarge': 523, 'm1.medium': 393, 'm1.small': 70 },
{ date: new Date(2020, 8, 30), 'm1.large': 811, 'm1.xlarge': 527, 'm1.medium': 353, 'm1.small': 88 },
export const cpuData = [
{ date: new Date(2020, 8, 16).getTime(), 'm1.large': 878, 'm1.xlarge': 491, 'm1.medium': 284, 'm1.small': 70 },
{ date: new Date(2020, 8, 17).getTime(), 'm1.large': 781, 'm1.xlarge': 435, 'm1.medium': 242, 'm1.small': 96 },
{ date: new Date(2020, 8, 18).getTime(), 'm1.large': 788, 'm1.xlarge': 478, 'm1.medium': 311, 'm1.small': 79 },
{ date: new Date(2020, 8, 19).getTime(), 'm1.large': 729, 'm1.xlarge': 558, 'm1.medium': 298, 'm1.small': 97 },
{ date: new Date(2020, 8, 20).getTime(), 'm1.large': 988, 'm1.xlarge': 530, 'm1.medium': 255, 'm1.small': 97 },
{ date: new Date(2020, 8, 21).getTime(), 'm1.large': 1016, 'm1.xlarge': 445, 'm1.medium': 339, 'm1.small': 70 },
{ date: new Date(2020, 8, 22).getTime(), 'm1.large': 987, 'm1.xlarge': 549, 'm1.medium': 273, 'm1.small': 62 },
{ date: new Date(2020, 8, 23).getTime(), 'm1.large': 986, 'm1.xlarge': 518, 'm1.medium': 341, 'm1.small': 67 },
{ date: new Date(2020, 8, 24).getTime(), 'm1.large': 925, 'm1.xlarge': 454, 'm1.medium': 382, 'm1.small': 68 },
{ date: new Date(2020, 8, 25).getTime(), 'm1.large': 742, 'm1.xlarge': 538, 'm1.medium': 361, 'm1.small': 70 },
{ date: new Date(2020, 8, 26).getTime(), 'm1.large': 920, 'm1.xlarge': 486, 'm1.medium': 262, 'm1.small': 91 },
{ date: new Date(2020, 8, 27).getTime(), 'm1.large': 826, 'm1.xlarge': 457, 'm1.medium': 248, 'm1.small': 76 },
{ date: new Date(2020, 8, 28).getTime(), 'm1.large': 698, 'm1.xlarge': 534, 'm1.medium': 243, 'm1.small': 66 },
{ date: new Date(2020, 8, 29).getTime(), 'm1.large': 1003, 'm1.xlarge': 523, 'm1.medium': 393, 'm1.small': 70 },
{ date: new Date(2020, 8, 30).getTime(), 'm1.large': 811, 'm1.xlarge': 527, 'm1.medium': 353, 'm1.small': 88 },
];

export const cpuDomain = cpuData.map(({ date }) => date);

export const cpuSeries: BarChartProps<Date>['series'] = [
export const cpuSeries = [
{
title: 'm1.large',
type: 'bar',
data: cpuData.map(datum => ({ x: datum.date, y: datum['m1.large'] })),
name: 'm1.large',
type: 'column',
data: cpuData.map(datum => datum['m1.large']),
},
{
title: 'm1.xlarge',
type: 'bar',
data: cpuData.map(datum => ({ x: datum.date, y: datum['m1.xlarge'] })),
name: 'm1.xlarge',
type: 'column',
data: cpuData.map(datum => datum['m1.xlarge']),
},
{
title: 'm1.medium',
type: 'bar',
data: cpuData.map(datum => ({ x: datum.date, y: datum['m1.medium'] })),
name: 'm1.medium',
type: 'column',
data: cpuData.map(datum => datum['m1.medium']),
},
{
title: 'm1.small',
type: 'bar',
data: cpuData.map(datum => ({ x: datum.date, y: datum['m1.small'] })),
name: 'm1.small',
type: 'column',
data: cpuData.map(datum => datum['m1.small']),
},
];
] as const;
67 changes: 38 additions & 29 deletions src/pages/dashboard/widgets/instance-hours/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@
// SPDX-License-Identifier: MIT-0
import React from 'react';

import BarChart from '@cloudscape-design/components/bar-chart';
import { CartesianChart } from '@cloudscape-design/chart-components';
import Header from '@cloudscape-design/components/header';
import Link from '@cloudscape-design/components/link';

import { barChartInstructions, commonChartProps, dateFormatter } from '../chart-commons';
import {
barChartInstructions,
commonChartProps,
dateFormatter,
numberTickFormatter,
useHighcharts,
} from '../chart-commons';
import { WidgetConfig } from '../interfaces';
import { cpuDomain, cpuSeries } from './data';
import { cpuData, cpuSeries } from './data';

function InstanceHoursHeader() {
return (
Expand All @@ -19,39 +25,42 @@ function InstanceHoursHeader() {
}

function InstanceHoursContent() {
const highcharts = useHighcharts();
return (
<BarChart
<CartesianChart
{...commonChartProps}
highcharts={highcharts}
stacking="normal"
fitHeight={true}
height={25}
yDomain={[0, 2000]}
xDomain={cpuDomain}
xScaleType="categorical"
stackedBars={true}
hideFilter={true}
series={cpuSeries}
xTitle="Date"
yTitle="Total instance hours"
chartHeight={25}
xAxis={{
type: 'category',
title: 'Date',
categories: cpuData.map(datum => dateFormatter(datum.date)),
}}
yAxis={{ title: 'Total instance hours', min: 0, max: 2000, valueFormatter: numberTickFormatter }}
ariaLabel="Instance hours"
ariaDescription={`Bar chart showing total instance hours per instance type over the last 15 days. ${barChartInstructions}`}
series={cpuSeries}
i18nStrings={{
...commonChartProps.i18nStrings,
filterLabel: 'Filter displayed instance types',
filterPlaceholder: 'Filter instance types',
xTickFormatter: dateFormatter,
chartRoleDescription: `Bar chart showing total instance hours per instance type over the last 15 days. ${barChartInstructions}`,
seriesFilterLabel: 'Filter displayed instance types',
seriesFilterPlaceholder: 'Filter instance types',
}}
tooltip={{
point: ({ item }) => ({
key: item.series.name,
value: (
<Link
external={true}
href="#"
ariaLabel={`See details for ${item.y} hours on ${item.series.name} (opens in a new tab)`}
>
{item.y}
</Link>
),
}),
}}
detailPopoverSeriesContent={({ series, y }) => ({
key: series.title,
value: (
<Link
external={true}
href="#"
ariaLabel={`See details for ${y} hours on ${series.title} (opens in a new tab)`}
>
{y}
</Link>
),
})}
/>
);
}
Expand Down
Loading
Loading