From 4599aa3c4502e5e5e5fb7434913f7ce9a6468547 Mon Sep 17 00:00:00 2001 From: Ahmed Kamel Date: Tue, 13 Feb 2024 20:20:22 +0000 Subject: [PATCH] feat(cloudwatch): add `TableWidget` (#29078) ### Issue # (if applicable) closes #28975. ### Reason for this change add support for table widget https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/add_remove_table_dashboard.html ### Description of changes add a new `TableWidget` and its supporting property classes/interfaces ### Description of how you validated changes added both unit/integ tests ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../TableWidget.assets.json | 19 ++ .../TableWidget.template.json | 55 ++++ ...efaultTestDeployAssertA94EAD9F.assets.json | 19 ++ ...aultTestDeployAssertA94EAD9F.template.json | 36 +++ .../integ.table-widget.js.snapshot/cdk.out | 1 + .../integ.table-widget.js.snapshot/integ.json | 12 + .../manifest.json | 113 +++++++ .../integ.table-widget.js.snapshot/tree.json | 136 ++++++++ .../aws-cloudwatch/test/integ.table-widget.ts | 36 +++ packages/aws-cdk-lib/aws-cloudwatch/README.md | 86 +++++ .../aws-cdk-lib/aws-cloudwatch/lib/graph.ts | 296 ++++++++++++++++++ .../aws-cloudwatch/test/graphs.test.ts | 226 ++++++++++++- packages/aws-cdk-lib/awslint.json | 1 + 13 files changed, 1035 insertions(+), 1 deletion(-) create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/TableWidget.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/TableWidget.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/TableWidgetTestDefaultTestDeployAssertA94EAD9F.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/TableWidgetTestDefaultTestDeployAssertA94EAD9F.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/integ.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/tree.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.ts diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/TableWidget.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/TableWidget.assets.json new file mode 100644 index 0000000000000..d319c29a78702 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/TableWidget.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "82a79adc47ff443d7d7136e30219be4a553a4b7c1fbe6fb9b083dc945fe35137": { + "source": { + "path": "TableWidget.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "82a79adc47ff443d7d7136e30219be4a553a4b7c1fbe6fb9b083dc945fe35137.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/TableWidget.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/TableWidget.template.json new file mode 100644 index 0000000000000..b9940ce70a787 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/TableWidget.template.json @@ -0,0 +1,55 @@ +{ + "Resources": { + "Dashboard9E4231ED": { + "Type": "AWS::CloudWatch::Dashboard", + "Properties": { + "DashboardBody": { + "Fn::Join": [ + "", + [ + "{\"widgets\":[{\"type\":\"metric\",\"width\":12,\"height\":12,\"x\":0,\"y\":0,\"properties\":{\"title\":\"Table\",\"view\":\"table\",\"table\":{\"layout\":\"horizontal\",\"showTimeSeriesData\":true,\"stickySummary\":false,\"summaryColumns\":[]},\"region\":\"", + { + "Ref": "AWS::Region" + }, + "\",\"metrics\":[[\"CDK/Test\",\"Metric\",{\"stat\":\"Minimum\"}]],\"annotations\":{\"horizontal\":[{\"value\":1000,\"color\":\"#d62728\",\"fill\":\"above\"},[{\"value\":500,\"color\":\"#ff7f0e\"},{\"value\":1000}],{\"value\":500,\"color\":\"#2ca02c\",\"fill\":\"below\"}]},\"yAxis\":{\"left\":{\"showUnits\":true}},\"singleValueFullPrecision\":true,\"period\":60}}]}" + ] + ] + } + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/TableWidgetTestDefaultTestDeployAssertA94EAD9F.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/TableWidgetTestDefaultTestDeployAssertA94EAD9F.assets.json new file mode 100644 index 0000000000000..6f24596c45492 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/TableWidgetTestDefaultTestDeployAssertA94EAD9F.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "TableWidgetTestDefaultTestDeployAssertA94EAD9F.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/TableWidgetTestDefaultTestDeployAssertA94EAD9F.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/TableWidgetTestDefaultTestDeployAssertA94EAD9F.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/TableWidgetTestDefaultTestDeployAssertA94EAD9F.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/cdk.out new file mode 100644 index 0000000000000..1f0068d32659a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"36.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/integ.json new file mode 100644 index 0000000000000..6e0ff930789f1 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "36.0.0", + "testCases": { + "TableWidgetTest/DefaultTest": { + "stacks": [ + "TableWidget" + ], + "assertionStack": "TableWidgetTest/DefaultTest/DeployAssert", + "assertionStackName": "TableWidgetTestDefaultTestDeployAssertA94EAD9F" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/manifest.json new file mode 100644 index 0000000000000..9bd0925759a55 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/manifest.json @@ -0,0 +1,113 @@ +{ + "version": "36.0.0", + "artifacts": { + "TableWidget.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "TableWidget.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "TableWidget": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "TableWidget.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/82a79adc47ff443d7d7136e30219be4a553a4b7c1fbe6fb9b083dc945fe35137.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "TableWidget.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "TableWidget.assets" + ], + "metadata": { + "/TableWidget/Dashboard/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Dashboard9E4231ED" + } + ], + "/TableWidget/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/TableWidget/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "TableWidget" + }, + "TableWidgetTestDefaultTestDeployAssertA94EAD9F.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "TableWidgetTestDefaultTestDeployAssertA94EAD9F.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "TableWidgetTestDefaultTestDeployAssertA94EAD9F": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "TableWidgetTestDefaultTestDeployAssertA94EAD9F.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "TableWidgetTestDefaultTestDeployAssertA94EAD9F.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "TableWidgetTestDefaultTestDeployAssertA94EAD9F.assets" + ], + "metadata": { + "/TableWidgetTest/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/TableWidgetTest/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "TableWidgetTest/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/tree.json new file mode 100644 index 0000000000000..004ef49d41df7 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.js.snapshot/tree.json @@ -0,0 +1,136 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "TableWidget": { + "id": "TableWidget", + "path": "TableWidget", + "children": { + "Dashboard": { + "id": "Dashboard", + "path": "TableWidget/Dashboard", + "children": { + "Resource": { + "id": "Resource", + "path": "TableWidget/Dashboard/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Dashboard", + "aws:cdk:cloudformation:props": { + "dashboardBody": { + "Fn::Join": [ + "", + [ + "{\"widgets\":[{\"type\":\"metric\",\"width\":12,\"height\":12,\"x\":0,\"y\":0,\"properties\":{\"title\":\"Table\",\"view\":\"table\",\"table\":{\"layout\":\"horizontal\",\"showTimeSeriesData\":true,\"stickySummary\":false,\"summaryColumns\":[]},\"region\":\"", + { + "Ref": "AWS::Region" + }, + "\",\"metrics\":[[\"CDK/Test\",\"Metric\",{\"stat\":\"Minimum\"}]],\"annotations\":{\"horizontal\":[{\"value\":1000,\"color\":\"#d62728\",\"fill\":\"above\"},[{\"value\":500,\"color\":\"#ff7f0e\"},{\"value\":1000}],{\"value\":500,\"color\":\"#2ca02c\",\"fill\":\"below\"}]},\"yAxis\":{\"left\":{\"showUnits\":true}},\"singleValueFullPrecision\":true,\"period\":60}}]}" + ] + ] + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudwatch.CfnDashboard", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudwatch.Dashboard", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "TableWidget/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "TableWidget/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, + "TableWidgetTest": { + "id": "TableWidgetTest", + "path": "TableWidgetTest", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "TableWidgetTest/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "TableWidgetTest/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "TableWidgetTest/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "TableWidgetTest/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "TableWidgetTest/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.ts new file mode 100644 index 0000000000000..d2cf61873252e --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudwatch/test/integ.table-widget.ts @@ -0,0 +1,36 @@ +import * as cdk from 'aws-cdk-lib/core'; +import * as integ from '@aws-cdk/integ-tests-alpha'; +import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; +import { Color, TableThreshold } from 'aws-cdk-lib/aws-cloudwatch'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'TableWidget'); + +const dashboard = new cloudwatch.Dashboard(stack, 'Dashboard'); + +const widget = new cloudwatch.TableWidget({ + title: 'Table', + metrics: [new cloudwatch.Metric({ + namespace: 'CDK/Test', + metricName: 'Metric', + statistic: 'Minimum', + })], + period: cdk.Duration.minutes(1), + height: 12, + width: 12, + showUnitsInLabel: true, + fullPrecision: true, + layout: cloudwatch.TableLayout.HORIZONTAL, + thresholds: [ + TableThreshold.above(1000, Color.RED), + TableThreshold.between(500, 1000, Color.ORANGE), + TableThreshold.below(500, Color.GREEN), + ], +}); + +dashboard.addWidgets(widget); + +new integ.IntegTest(app, 'TableWidgetTest', { + testCases: [stack], +}); diff --git a/packages/aws-cdk-lib/aws-cloudwatch/README.md b/packages/aws-cdk-lib/aws-cloudwatch/README.md index c974fa1adde5a..be53d4f3becdd 100644 --- a/packages/aws-cdk-lib/aws-cloudwatch/README.md +++ b/packages/aws-cdk-lib/aws-cloudwatch/README.md @@ -496,6 +496,92 @@ dashboard.addWidgets(new cloudwatch.GraphWidget({ })); ``` +### Table Widget + +A `TableWidget` can display any number of metrics in tabular form. + +```ts +declare const dashboard: cloudwatch.Dashboard; +declare const executionCountMetric: cloudwatch.Metric; + +dashboard.addWidgets(new cloudwatch.TableWidget({ + title: "Executions", + metrics: [executionCountMetric], +})); +``` + +The `layout` property can be used to invert the rows and columns of the table. +The default `cloudwatch.TableLayout.HORIZONTAL` means that metrics are shown in rows and datapoints in columns. +`cloudwatch.TableLayout.VERTICAL` means that metrics are shown in columns and datapoints in rows. + +```ts +declare const dashboard: cloudwatch.Dashboard; + +dashboard.addWidgets(new cloudwatch.TableWidget({ + // ... + + layout: cloudwatch.TableLayout.VERTICAL, +})); +``` + +The `summary` property allows customizing the table to show summary columns (`columns` sub property), +whether to make the summary columns sticky remaining in view while scrolling (`sticky` sub property), +and to optionally only present summary columns (`hideNonSummaryColumns` sub property). + +```ts +declare const dashboard: cloudwatch.Dashboard; + +dashboard.addWidgets(new cloudwatch.TableWidget({ + // ... + + summary: { + columns: [cloudwatch.TableSummaryColumn.AVERAGE], + hideNonSummaryColumns: true, + sticky: true, + }, +})); +``` + +The `thresholds` property can be used to highlight cells with a color when the datapoint value falls within the threshold. + +```ts +declare const dashboard: cloudwatch.Dashboard; + +dashboard.addWidgets(new cloudwatch.TableWidget({ + // ... + + thresholds: [ + cloudwatch.TableThreshold.above(1000, cloudwatch.Color.RED), + cloudwatch.TableThreshold.between(500, 1000, cloudwatch.Color.ORANGE), + cloudwatch.TableThreshold.below(500, cloudwatch.Color.GREEN), + ], +})); +``` + +The `showUnitsInLabel` property can be used to display what unit is associated with a metric in the label column. + +```ts +declare const dashboard: cloudwatch.Dashboard; + +dashboard.addWidgets(new cloudwatch.TableWidget({ + // ... + + showUnitsInLabel: true, +})); +``` + +The `fullPrecision` property can be used to show as many digits as can fit in a cell, before rounding. + +```ts +declare const dashboard: cloudwatch.Dashboard; + +dashboard.addWidgets(new cloudwatch.TableWidget({ + // ... + + fullPrecision: true, +})); +``` + ### Gauge widget Gauge graph requires the max and min value of the left Y axis, if no value is informed the limits will be from 0 to 100. diff --git a/packages/aws-cdk-lib/aws-cloudwatch/lib/graph.ts b/packages/aws-cdk-lib/aws-cloudwatch/lib/graph.ts index bd571611ff3eb..008513b30d9cc 100644 --- a/packages/aws-cdk-lib/aws-cloudwatch/lib/graph.ts +++ b/packages/aws-cdk-lib/aws-cloudwatch/lib/graph.ts @@ -519,6 +519,302 @@ export class GraphWidget extends ConcreteWidget { } } +/** + * Layout for TableWidget + */ +export enum TableLayout { + /** + * Data points are laid out in columns + */ + HORIZONTAL = 'horizontal', + + /** + * Data points are laid out in rows + */ + VERTICAL = 'vertical', +} + +/** + * Standard table summary columns + */ +export enum TableSummaryColumn { + /** + * Minimum of all data points + */ + MINIMUM = 'MIN', + + /** + * Maximum of all data points + */ + MAXIMUM = 'MAX', + + /** + * Sum of all data points + */ + SUM = 'SUM', + + /** + * Average of all data points + */ + AVERAGE = 'AVG', +} + +/** + * Properties for TableWidget's summary columns + */ +export interface TableSummaryProps { + /** + * Summary columns + * + * @default - No summary columns will be shown + */ + readonly columns?: TableSummaryColumn[]; + + /** + * Make the summary columns sticky, so that they remain in view while scrolling + * + * @default - false + */ + readonly sticky?: boolean; + + /** + * Prevent the columns of datapoints from being displayed, so that only the label and summary columns are displayed + * + * @default - false + */ + readonly hideNonSummaryColumns?: boolean; +} + +/** + * Thresholds for highlighting cells in TableWidget + */ +export class TableThreshold { + /** + * A threshold for highlighting and coloring cells above the specified value + * + * @param value lower bound of threshold range + * @param color cell color for values within threshold range + */ + public static above(value: number, color?: string): TableThreshold { + return new TableThreshold(value, undefined, color, Shading.ABOVE); + } + + /** + * A threshold for highlighting and coloring cells below the specified value + * + * @param value upper bound of threshold range + * @param color cell color for values within threshold range + */ + public static below(value: number, color?: string): TableThreshold { + return new TableThreshold(value, undefined, color, Shading.BELOW); + } + + /** + * A threshold for highlighting and coloring cells within the specified values + * + * @param lowerBound lower bound of threshold range + * @param upperBound upper bound of threshold range + * @param color cell color for values within threshold range + */ + public static between(lowerBound: number, upperBound: number, color?: string): TableThreshold { + return new TableThreshold(lowerBound, upperBound, color); + } + + private readonly lowerBound: number; + private readonly upperBound?: number; + private readonly color?: string; + private readonly comparator?: Shading; + + private constructor(lowerBound: number, upperBound?: number, color?: string, comparator?: Shading) { + this.lowerBound = lowerBound; + this.upperBound = upperBound; + this.color = color; + this.comparator = comparator; + } + + public toJson(): any { + if (this.upperBound) { + return [ + { value: this.lowerBound, color: this.color }, + { value: this.upperBound }, + ]; + } + return { value: this.lowerBound, color: this.color, fill: this.comparator }; + } +} + +/** + * Properties for a TableWidget + */ +export interface TableWidgetProps extends MetricWidgetProps { + /** + * Table layout + * + * @default - TableLayout.HORIZONTAL + */ + readonly layout?: TableLayout; + + /** + * Properties for displaying summary columns + * + * @default - no summary columns are shown + */ + readonly summary?: TableSummaryProps; + + /** + * Thresholds for highlighting table cells + * + * @default - No thresholds + */ + readonly thresholds?: TableThreshold[]; + + /** + * Show the metrics units in the label column + * + * @default - false + */ + readonly showUnitsInLabel?: boolean; + + /** + * Metrics to display in the table + * + * @default - No metrics + */ + readonly metrics?: IMetric[]; + + /** + * Whether the graph should show live data + * + * @default false + */ + readonly liveData?: boolean; + + /** + * Whether to show as many digits as can fit, before rounding. + * + * @default false + */ + readonly fullPrecision?: boolean; + + /** + * Whether to show the value from the entire time range. Only applicable for Bar and Pie charts. + * + * If false, values will be from the most recent period of your chosen time range; + * if true, shows the value from the entire time range. + * + * @default false + */ + readonly setPeriodToTimeRange?: boolean; + + /** + * The default period for all metrics in this widget. + * The period is the length of time represented by one data point on the graph. + * This default can be overridden within each metric definition. + * + * @default cdk.Duration.seconds(300) + */ + readonly period?: cdk.Duration; + + /** + * The default statistic to be displayed for each metric. + * This default can be overridden within the definition of each individual metric + * + * @default - The statistic for each metric is used + */ + readonly statistic?: string; + + /** + * The start of the time range to use for each widget independently from those of the dashboard. + * You can specify start without specifying end to specify a relative time range that ends with the current time. + * In this case, the value of start must begin with -P, and you can use M, H, D, W and M as abbreviations for + * minutes, hours, days, weeks and months. For example, -PT8H shows the last 8 hours and -P3M shows the last three months. + * You can also use start along with an end field, to specify an absolute time range. + * When specifying an absolute time range, use the ISO 8601 format. For example, 2018-12-17T06:00:00.000Z. + * + * @default When the dashboard loads, the start time will be the default time range. + */ + readonly start?: string; + + /** + * The end of the time range to use for each widget independently from those of the dashboard. + * If you specify a value for end, you must also specify a value for start. + * Specify an absolute time in the ISO 8601 format. For example, 2018-12-17T06:00:00.000Z. + * + * @default When the dashboard loads, the end date will be the current time. + */ + readonly end?: string; +} + +/** + * A dashboard widget that displays metrics + */ +export class TableWidget extends ConcreteWidget { + + private readonly metrics: IMetric[]; + + constructor(private readonly props: TableWidgetProps) { + super(props.width || 6, props.height || 6); + + this.props = props; + this.metrics = props.metrics ?? []; + this.copyMetricWarnings(...this.metrics); + + if (props.end !== undefined && props.start === undefined) { + throw new Error('If you specify a value for end, you must also specify a value for start.'); + } + } + + /** + * Add another metric + * + * @param metric the metric to add + */ + public addMetric(metric: IMetric) { + this.metrics.push(metric); + this.copyMetricWarnings(metric); + } + + public toJson(): any[] { + const horizontalAnnotations = (this.props.thresholds ?? []).map(threshold => threshold.toJson()); + const annotations = horizontalAnnotations.length > 0 ? ({ + horizontal: horizontalAnnotations, + }) : undefined; + const metrics = allMetricsGraphJson(this.metrics, []); + return [{ + type: 'metric', + width: this.width, + height: this.height, + x: this.x, + y: this.y, + properties: { + title: this.props.title, + view: 'table', + table: { + layout: this.props.layout ?? TableLayout.HORIZONTAL, + showTimeSeriesData: !(this.props.summary?.hideNonSummaryColumns ?? false), + stickySummary: this.props.summary?.sticky ?? false, + summaryColumns: this.props.summary?.columns ?? [], + }, + region: this.props.region || cdk.Aws.REGION, + metrics: metrics.length > 0 ? metrics : undefined, + annotations, + yAxis: { + left: this.props.showUnitsInLabel ? { + showUnits: true, + } : undefined, + }, + liveData: this.props.liveData, + singleValueFullPrecision: this.props.fullPrecision, + setPeriodToTimeRange: this.props.setPeriodToTimeRange, + period: this.props.period?.toSeconds(), + stat: this.props.statistic, + start: this.props.start, + end: this.props.end, + }, + }]; + } +} + /** * Properties for a SingleValueWidget */ diff --git a/packages/aws-cdk-lib/aws-cloudwatch/test/graphs.test.ts b/packages/aws-cdk-lib/aws-cloudwatch/test/graphs.test.ts index 72ff2cca8a0ac..dd70bce845df0 100644 --- a/packages/aws-cdk-lib/aws-cloudwatch/test/graphs.test.ts +++ b/packages/aws-cdk-lib/aws-cloudwatch/test/graphs.test.ts @@ -1,5 +1,24 @@ import { Duration, Stack } from '../../core'; -import { Alarm, AlarmWidget, Color, GraphWidget, GraphWidgetView, LegendPosition, LogQueryWidget, Metric, Shading, SingleValueWidget, LogQueryVisualizationType, CustomWidget, GaugeWidget, VerticalShading } from '../lib'; +import { + Alarm, + AlarmWidget, + Color, + CustomWidget, + GaugeWidget, + GraphWidget, + GraphWidgetView, + LegendPosition, + LogQueryVisualizationType, + LogQueryWidget, + Metric, + Shading, + SingleValueWidget, + TableLayout, + TableSummaryColumn, + TableThreshold, + TableWidget, + VerticalShading, +} from '../lib'; describe('Graphs', () => { test('add stacked property to graphs', () => { @@ -1086,4 +1105,209 @@ describe('Graphs', () => { }, }]); }); + + describe('TableWidget', () => { + let stack; + let metric; + + beforeEach(() => { + stack = new Stack(); + metric = new Metric({ namespace: 'CDK', metricName: 'Test' }); + }); + + test('with optional fields unset', () => { + // GIVEN + const widget = new TableWidget({ + metrics: [metric], + }); + + // THEN + expect(stack.resolve(widget.toJson())).toEqual([{ + type: 'metric', + height: 6, + width: 6, + properties: { + view: 'table', + metrics: [ + ['CDK', 'Test'], + ], + region: { Ref: 'AWS::Region' }, + table: { + layout: 'horizontal', + showTimeSeriesData: true, + stickySummary: false, + summaryColumns: [], + }, + yAxis: {}, + }, + }]); + }); + + test('add metrics lazily', () => { + // GIVEN + const widget = new TableWidget({}); + widget.addMetric(metric); + + // THEN + expect(stack.resolve(widget.toJson())).toEqual([{ + type: 'metric', + height: 6, + width: 6, + properties: { + view: 'table', + metrics: [ + ['CDK', 'Test'], + ], + region: { Ref: 'AWS::Region' }, + table: { + layout: 'horizontal', + showTimeSeriesData: true, + stickySummary: false, + summaryColumns: [], + }, + yAxis: {}, + }, + }]); + }); + + test('with most table fields set', () => { + // GIVEN + const widget = new TableWidget({ + metrics: [metric], + layout: TableLayout.VERTICAL, + showUnitsInLabel: true, + liveData: true, + fullPrecision: true, + summary: { + columns: [TableSummaryColumn.AVERAGE], + hideNonSummaryColumns: true, + sticky: true, + }, + }); + + // THEN + expect(stack.resolve(widget.toJson())).toEqual([{ + type: 'metric', + height: 6, + width: 6, + properties: { + view: 'table', + metrics: [ + ['CDK', 'Test'], + ], + region: { Ref: 'AWS::Region' }, + liveData: true, + singleValueFullPrecision: true, + table: { + layout: 'vertical', + showTimeSeriesData: false, + stickySummary: true, + summaryColumns: ['AVG'], + }, + yAxis: { + left: { + showUnits: true, + }, + }, + }, + }]); + }); + + test('with thresholds', () => { + // GIVEN + const widget = new TableWidget({ + metrics: [metric], + thresholds: [ + TableThreshold.above(1000, Color.RED), + TableThreshold.between(500, 1000, Color.ORANGE), + TableThreshold.below(500, Color.GREEN), + ], + }); + + // THEN + expect(stack.resolve(widget.toJson())).toEqual([{ + type: 'metric', + height: 6, + width: 6, + properties: { + view: 'table', + metrics: [ + ['CDK', 'Test'], + ], + region: { Ref: 'AWS::Region' }, + table: { + layout: 'horizontal', + showTimeSeriesData: true, + stickySummary: false, + summaryColumns: [], + }, + yAxis: {}, + annotations: { + horizontal: [ + { + color: '#d62728', + fill: 'above', + value: 1000, + }, + [ + { + color: '#ff7f0e', + value: 500, + }, + { + value: 1000, + }, + ], + { + color: '#2ca02c', + fill: 'below', + value: 500, + }, + ], + }, + }, + }]); + }); + + test('with start and end set', () => { + // GIVEN + const widget = new TableWidget({ + metrics: [metric], + start: '-P7D', + end: '2018-12-17T06:00:00.000Z', + }); + + // THEN + expect(stack.resolve(widget.toJson())).toEqual([{ + type: 'metric', + height: 6, + width: 6, + properties: { + view: 'table', + metrics: [ + ['CDK', 'Test'], + ], + region: { Ref: 'AWS::Region' }, + table: { + layout: 'horizontal', + showTimeSeriesData: true, + stickySummary: false, + summaryColumns: [], + }, + yAxis: {}, + start: '-P7D', + end: '2018-12-17T06:00:00.000Z', + }, + }]); + }); + + test('cannot specify an end without a start', () => { + expect(() => { + new TableWidget({ + metrics: [metric], + end: '2018-12-17T06:00:00.000Z', + }); + }).toThrow(/If you specify a value for end, you must also specify a value for start./); + }); + }); }); diff --git a/packages/aws-cdk-lib/awslint.json b/packages/aws-cdk-lib/awslint.json index 4798a4f3fc2fd..2c108cd3a92da 100644 --- a/packages/aws-cdk-lib/awslint.json +++ b/packages/aws-cdk-lib/awslint.json @@ -240,6 +240,7 @@ "docs-public-apis:aws-cdk-lib.aws_cloudtrail.InsightType.value", "docs-public-apis:aws-cdk-lib.aws_cloudwatch.DefaultValue.val", "docs-public-apis:aws-cdk-lib.aws_cloudwatch.Values.toJson", + "docs-public-apis:aws-cdk-lib.aws_cloudwatch.TableThreshold.toJson", "docs-public-apis:aws-cdk-lib.aws_codebuild.Artifacts.s3", "docs-public-apis:aws-cdk-lib.aws_codebuild.BuildSpec.fromObject", "docs-public-apis:aws-cdk-lib.aws_codebuild.Cache.none",