├── .env.example
├── .eslintrc.yml
├── .gitignore
├── LICENSE
├── README.md
├── img
└── screenshot.png
├── index.ejs
├── package-lock.json
├── package.json
├── poi.config.js
├── server
├── archive.zip
├── index.js
├── package-lock.json
└── package.json
├── src
├── components
│ ├── App.vue
│ ├── ChartTile.vue
│ ├── EngrInt.vue
│ ├── LabelledRatio.vue
│ ├── LineChart.vue
│ ├── PercentileChange.vue
│ ├── RelativeDate.vue
│ ├── Tile.vue
│ └── ValueTile.vue
├── index.js
├── metrics.example.json
├── pwa.js
├── services
│ ├── CloudWatchService.js
│ └── DatasetArray.js
└── styles
│ └── vars.scss
└── static
├── favicon.ico
├── fonts
├── XRXV3I6Li01BKofINeaBTMnFcQ.woff2
├── XRXV3I6Li01BKofIO-aBTMnFcQIG.woff2
├── XRXW3I6Li01BKofA6sKUYevIWzgPDA.woff2
├── XRXW3I6Li01BKofA6sKUb-vIWzgPDEtj.woff2
├── XRXW3I6Li01BKofAtsGUYevIWzgPDA.woff2
├── XRXW3I6Li01BKofAtsGUb-vIWzgPDEtj.woff2
└── nunito.css
├── icons
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── ios-icon-120.png
├── ios-icon-152.png
├── ios-icon-167.png
└── ios-icon-180.png
├── manifest.json
└── style.css
/.env.example:
--------------------------------------------------------------------------------
1 | API_ENDPOINT=
2 | API_KEY=
3 |
--------------------------------------------------------------------------------
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | extends:
2 | - airbnb-base
3 | - plugin:vue/recommended
4 |
5 | parserOptions:
6 | ecmaVersion: 8
7 |
8 | settings:
9 | import/core-modules:
10 | - vue
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | .DS_Store
4 | dist
5 | .env.*
6 | !.env.example
7 | src/metrics.json
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Ashton Meuser
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vue CloudWatch Dashboard
2 |
3 | 
4 |
5 | This project facilitates live monitoring CloudWatch metrics. All desired metrics are specified in JSON format.
6 |
7 | PWA functionality allows the app to be cached on a mobile device after it's been served via `npm run serve` command.
8 |
9 | A major problem with accessing CloudWatch metrics data is that metrics are often only reported when an event occurs. This results in data with many missing datapoints. The server-side Lambda function included in the project pads data with zero values whenever it is found to be missing.
10 |
11 | The period, backfill time, and refresh interval are all configurable. They have been set with reasonable values of five minutes, five minutes, and two hours, respectively. Please note that AWS promises that CloudWatch metrics will be populated with a latency of at most ten minutes.
12 |
13 | The charting capabilities of this project are limited in favor of a simple means of interacting with CloudWatch metrics data. More charting functionality may be added at a later date.
14 |
15 | This project's aesthetic was heavily inspired by [Vuepoint](https://github.com/ashtonmeuser/vuepoint).
16 |
17 | ## Commands
18 |
19 | First, clone the repo. Run `npm install` to install dependencies. Ensure the rest of the setup is complete. The steps to setup are as follows.
20 |
21 | * Create a `metrics.json` file appropriate to the metrics you'd like to pull from CloudWatch.
22 | * Create `.env.{NODE_ENV}` file(s) appropriate to the way(s) in which you intend to build the web application. A `.env.development` file and `.env.production` file are recommended.
23 | * The lambda function uploaded to AWS. You can simply use the included [archive](server/archive.zip).
24 | * An API properly set up using API Gateway. See the *Lambda Function* section for more details.
25 |
26 | To build for development including hot reload, run the following.
27 |
28 | ```bash
29 | # build for development
30 | npm run dev
31 | ```
32 |
33 | To fully optimize the web application, run the following. This bundles the application into a `dist` directory. The production build also includes PWA support, which allows the web application to be cahced on a mobile device. Once the application is being served, access it on a mobile device and save it to your homescreen. The server can now be taken down as the application is cached and fully functional on your device.
34 |
35 | ```bash
36 | # build for production
37 | npm run build
38 |
39 | # serve the bundled dist folder in production mode
40 | npm run serve
41 | ```
42 |
43 | ## Credentials
44 |
45 | The app expects there to be a `.env.{NODE_ENV}` file in the following format:
46 |
47 | ```
48 | API_ENDPOINT={YOUR_API_ENDPOINT}
49 | API_KEY={YOUR_API_KEY}
50 | ```
51 |
52 | Replace `NODE_ENV` with `development`, `test`, or `production`. These will be loaded when built for the respective environments and used to interact with your API. See the next section for setting up an endpoint using AWS Gateway and Lambda.
53 |
54 | ## Lambda Function
55 |
56 | An AWS Lambda function can be used to serve properly formatted metrics to the CloudWatch Dashboard. Please see the [server](server) directory. To deploy, upload the [archive](server/archive.zip) to a Lambda function either via the AWS CLI or the web interface. If you make changes to the server-side logic, make sure you recreate and reupload the archive.
57 |
58 | Create an endpoint using AWS API Gateway and properly secure it with an API key or other security measure. After creating your API endpoint, add a POST method and select the lambda function you just created. Enable *Use Lambda Proxy integration*. Next, enable CORS via the action dropdown menu. If using an API key (recommended), create a usage plan. This will prompt you to create an API key and select an API. Name your key and select the API you just created. Note the endpoint URL and the API you created. These should be entered into the `.env.{NODE_ENV}` file.
59 |
60 | Please note that AWS charges for accessing CloudWatch metrics, so securing your endpoint should be a priority. This should also influence the refresh rate you choose. CloudWatch pricing information can be found [here](https://aws.amazon.com/cloudwatch/pricing/).
61 |
62 | ## AWS IAM Permissions
63 |
64 | The Lambda function must have access to the `cloudwatch:GetMetricData` permission. Create a role in IAM to allow Lambda to access the correct resources.
65 |
--------------------------------------------------------------------------------
/img/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ashtonmeuser/vue-cloudwatch-dashboard/4dcc2ededc1e9eca7654fd76387ed4442202ebd6/img/screenshot.png
--------------------------------------------------------------------------------
/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | <%= htmlWebpackPlugin.options.title %>
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | <% for (var chunk of webpack.chunks) {
23 | for (var file of chunk.files) {
24 | if (file.match(/\.(js|css)$/)) { %>
25 |
26 | <% }}} %>
27 |
28 |
29 |
30 |
31 | You need to enable JavaScript to run this app.
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "vue-cloudwatch-dashboard",
4 | "productName": "CloudWatch Dashboard",
5 | "description": "A dashboard to display AWS CloudWatch metrics built with Vue.",
6 | "version": "0.0.0",
7 | "license": "MIT",
8 | "scripts": {
9 | "dev": "poi",
10 | "build": "poi build",
11 | "serve": "serve dist"
12 | },
13 | "author": {
14 | "name": "ashtonmeuser",
15 | "email": "ashtonmeuser@gmail.com"
16 | },
17 | "dependencies": {
18 | "axios": "^0.18.0",
19 | "chart.js": "^2.7.2",
20 | "normalize.css": "^8.0.0",
21 | "object-assign": "^4.1.1",
22 | "object-assign-deep": "^0.4.0",
23 | "offline-plugin": "^5.0.5",
24 | "promise-polyfill": "^8.1.0",
25 | "vue-axios": "^2.1.4",
26 | "vue-chartjs": "^3.4.0"
27 | },
28 | "devDependencies": {
29 | "@poi/plugin-offline": "^10.0.1",
30 | "dotenv": "^6.1.0",
31 | "eslint": "^5.6.1",
32 | "eslint-config-airbnb-base": "^13.1.0",
33 | "eslint-plugin-import": "^2.14.0",
34 | "eslint-plugin-vue": "^5.0.0-beta.3",
35 | "node-sass": "^4.9.3",
36 | "poi": "^10.2.10",
37 | "sass-loader": "^7.1.0",
38 | "serve": "^10.0.2"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/poi.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const pkg = require('./package');
3 |
4 | module.exports = {
5 | entry: 'src/index.js',
6 | html: {
7 | title: pkg.productName,
8 | description: pkg.description,
9 | template: path.join(__dirname, 'index.ejs'),
10 | },
11 | plugins: [
12 | require('@poi/plugin-offline')(), // eslint-disable-line
13 | ],
14 | };
15 |
--------------------------------------------------------------------------------
/server/archive.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ashtonmeuser/vue-cloudwatch-dashboard/4dcc2ededc1e9eca7654fd76387ed4442202ebd6/server/archive.zip
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const AWS = require('aws-sdk'); // eslint-disable-line import/no-unresolved
2 | const d3Scale = require('d3-scale');
3 | const d3Time = require('d3-time');
4 |
5 | const formatZeroData = date => ({ t: date, y: 0 });
6 |
7 | const linearZeroSeries = (start, end, periodMinutes) => {
8 | // Array of linearly displaced timestamps that we will project CloudWatch data onto
9 | const scale = d3Scale.scaleTime().domain([start, end]);
10 | const ticks = scale.ticks(d3Time.timeMinute.every(periodMinutes));
11 | const zeros = ticks.map(formatZeroData);
12 |
13 | return zeros;
14 | };
15 |
16 | const matchTimestampOrZero = (target, timestamps, values) => {
17 | // Lookup value at index of target in timestamps or return zero
18 | const targetTimestamp = target.getTime();
19 | const index = timestamps.findIndex(timestamp => timestamp.getTime() === targetTimestamp);
20 | return index < 0 ? 0 : values[index];
21 | };
22 |
23 | const minimumDateRange = (startString, periodMinutes) => {
24 | // CloudWatch can take up to 10 minutes to display metrics, set date range accordingly
25 | // Adjust end date by half period to ensure most recent data is fetched
26 | const halfPeriod = (periodMinutes * 60000) / 2;
27 | const end = new Date(Date.now() + halfPeriod);
28 | let start = new Date(startString);
29 | if (end - start < 10 * 60000) {
30 | start = new Date(end.getTime() - (10 * 60000));
31 | }
32 | return { start, end };
33 | };
34 |
35 | const getMetrics = async (cloudWatch, dateRange, requestMetrics, periodMinutes) => {
36 | const zeros = linearZeroSeries(dateRange.start, dateRange.end, periodMinutes);
37 | const params = {
38 | StartTime: zeros[0].t,
39 | MaxDatapoints: 100800, // Maximum 100800
40 | EndTime: zeros[zeros.length - 1].t,
41 | MetricDataQueries: requestMetrics.map(metric => ({
42 | Id: metric.id,
43 | MetricStat: {
44 | Metric: {
45 | Dimensions: [
46 | {
47 | Name: metric.dimensionName,
48 | Value: metric.dimensionValue,
49 | },
50 | ],
51 | MetricName: metric.name,
52 | Namespace: metric.namespace,
53 | },
54 | Period: periodMinutes * 60,
55 | Stat: metric.stat || 'Sum',
56 | },
57 | ReturnData: true,
58 | })),
59 | ScanBy: 'TimestampAscending',
60 | };
61 | const data = await cloudWatch.getMetricData(params).promise();
62 | const formatted = data.MetricDataResults.map(metric => ({
63 | id: metric.Id,
64 | data: zeros.map(zero => ({
65 | t: zero.t,
66 | y: matchTimestampOrZero(zero.t, metric.Timestamps, metric.Values),
67 | })),
68 | }));
69 |
70 | return formatted;
71 | };
72 |
73 | const handler = async (event, context, callback) => {
74 | const cloudWatch = new AWS.CloudWatch();
75 | const headers = {
76 | 'Access-Control-Allow-Origin': '*',
77 | 'Content-Type': 'application/json',
78 | };
79 |
80 | try {
81 | const data = JSON.parse(event.body);
82 | const metrics = await getMetrics(
83 | cloudWatch,
84 | minimumDateRange(data.start, data.periodMinutes),
85 | data.metrics,
86 | data.periodMinutes,
87 | );
88 | return callback(null, {
89 | statusCode: 200,
90 | headers,
91 | body: JSON.stringify(metrics),
92 | });
93 | } catch (error) {
94 | return callback(null, {
95 | statusCode: 500,
96 | headers,
97 | body: JSON.stringify({ error }),
98 | });
99 | }
100 | };
101 |
102 | module.exports = {
103 | handler,
104 | };
105 |
--------------------------------------------------------------------------------
/server/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "d3-array": {
8 | "version": "1.2.1",
9 | "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.1.tgz",
10 | "integrity": "sha512-CyINJQ0SOUHojDdFDH4JEM0552vCR1utGyLHegJHyYH0JyCpSeTPxi4OBqHMA2jJZq4NH782LtaJWBImqI/HBw=="
11 | },
12 | "d3-collection": {
13 | "version": "1.0.4",
14 | "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.4.tgz",
15 | "integrity": "sha1-NC39EoN8kJdPM/HMCnha6lcNzcI="
16 | },
17 | "d3-color": {
18 | "version": "1.2.0",
19 | "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.2.0.tgz",
20 | "integrity": "sha512-dmL9Zr/v39aSSMnLOTd58in2RbregCg4UtGyUArvEKTTN6S3HKEy+ziBWVYo9PTzRyVW+pUBHUtRKz0HYX+SQg=="
21 | },
22 | "d3-format": {
23 | "version": "1.3.0",
24 | "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.3.0.tgz",
25 | "integrity": "sha512-ycfLEIzHVZC3rOvuBOKVyQXSiUyCDjeAPIj9n/wugrr+s5AcTQC2Bz6aKkubG7rQaQF0SGW/OV4UEJB9nfioFg=="
26 | },
27 | "d3-interpolate": {
28 | "version": "1.2.0",
29 | "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.2.0.tgz",
30 | "integrity": "sha512-zLvTk8CREPFfc/2XglPQriAsXkzoRDAyBzndtKJWrZmHw7kmOWHNS11e40kPTd/oGk8P5mFJW5uBbcFQ+ybxyA==",
31 | "requires": {
32 | "d3-color": "1"
33 | }
34 | },
35 | "d3-scale": {
36 | "version": "2.0.0",
37 | "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.0.0.tgz",
38 | "integrity": "sha512-Sa2Ny6CoJT7x6dozxPnvUQT61epGWsgppFvnNl8eJEzfJBG0iDBBTJAtz2JKem7Mb+NevnaZiDiIDHsuWkv6vg==",
39 | "requires": {
40 | "d3-array": "^1.2.0",
41 | "d3-collection": "1",
42 | "d3-format": "1",
43 | "d3-interpolate": "1",
44 | "d3-time": "1",
45 | "d3-time-format": "2"
46 | }
47 | },
48 | "d3-time": {
49 | "version": "1.0.8",
50 | "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.8.tgz",
51 | "integrity": "sha512-YRZkNhphZh3KcnBfitvF3c6E0JOFGikHZ4YqD+Lzv83ZHn1/u6yGenRU1m+KAk9J1GnZMnKcrtfvSktlA1DXNQ=="
52 | },
53 | "d3-time-format": {
54 | "version": "2.1.1",
55 | "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.1.tgz",
56 | "integrity": "sha512-8kAkymq2WMfzW7e+s/IUNAtN/y3gZXGRrdGfo6R8NKPAA85UBTxZg5E61bR6nLwjPjj4d3zywSQe1CkYLPFyrw==",
57 | "requires": {
58 | "d3-time": "1"
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-cloudwatch-dashboard-server",
3 | "version": "1.0.0",
4 | "description": "Lambda function for Vue CloudWatch Dashboard",
5 | "scripts": {
6 | "test": "echo \"Error: no test specified\" && exit 1"
7 | },
8 | "author": "Ashton Meuser",
9 | "license": "MIT",
10 | "dependencies": {
11 | "d3-scale": "^2.0.0",
12 | "d3-time": "^1.0.8"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 | proj.
11 |
12 |
13 |
14 |
15 |
16 |
19 |
24 |
29 |
30 |
37 |
43 |
44 | {{ service.datasets.tagged('errors').sum() }} total
45 |
46 |
47 |
48 |
49 |
50 |
55 |
56 | {{ service.datasets.tagged('duration').max().toFixed(0) }}ms max.
57 |
58 |
59 |
60 |
61 |
62 |
69 |
76 |
77 | Updated
78 |
79 |
80 |
81 |
82 |
134 |
135 |
136 |
163 |
--------------------------------------------------------------------------------
/src/components/ChartTile.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
13 |
14 |
15 |
70 |
71 |
80 |
--------------------------------------------------------------------------------
/src/components/EngrInt.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
31 |
--------------------------------------------------------------------------------
/src/components/LabelledRatio.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
43 |
44 |
78 |
--------------------------------------------------------------------------------
/src/components/LineChart.vue:
--------------------------------------------------------------------------------
1 |
106 |
--------------------------------------------------------------------------------
/src/components/PercentileChange.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 | {{ Math.abs(percent).toFixed(decimalPlaces) }}%
6 |
7 |
8 |
9 |
55 |
56 |
68 |
--------------------------------------------------------------------------------
/src/components/RelativeDate.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
52 |
--------------------------------------------------------------------------------
/src/components/Tile.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
46 |
47 |
82 |
--------------------------------------------------------------------------------
/src/components/ValueTile.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
52 |
53 |
71 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import axios from 'axios';
3 | import VueAxios from 'vue-axios';
4 | import App from './components/App.vue';
5 |
6 | Vue.config.productionTip = false;
7 | Vue.use(VueAxios, axios);
8 |
9 | new Vue({ // eslint-disable-line no-new
10 | el: '#app',
11 | render: h => h(App),
12 | });
13 |
--------------------------------------------------------------------------------
/src/metrics.example.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "invocationsCreate",
4 | "tags": ["invocations"],
5 | "label": "Create",
6 | "dimensionName": "FunctionName",
7 | "dimensionValue": "fn-name-create",
8 | "name": "Invocations",
9 | "namespace": "AWS/Lambda"
10 | },
11 | {
12 | "id": "invocationsGet",
13 | "tags": ["invocations"],
14 | "label": "Get",
15 | "dimensionName": "FunctionName",
16 | "dimensionValue": "fn-name-get",
17 | "name": "Invocations",
18 | "namespace": "AWS/Lambda"
19 | },
20 | {
21 | "id": "durationCreate",
22 | "tags": ["duration"],
23 | "label": "Create",
24 | "dimensionName": "FunctionName",
25 | "dimensionValue": "fn-name-create",
26 | "name": "Duration",
27 | "namespace": "AWS/Lambda",
28 | "stat": "Average"
29 | },
30 | {
31 | "id": "durationGet",
32 | "tags": ["duration"],
33 | "label": "Get",
34 | "dimensionName": "FunctionName",
35 | "dimensionValue": "fn-name-get",
36 | "name": "Duration",
37 | "namespace": "AWS/Lambda",
38 | "stat": "Average"
39 | }
40 | ]
41 |
--------------------------------------------------------------------------------
/src/pwa.js:
--------------------------------------------------------------------------------
1 | import runtime from 'offline-plugin/runtime';
2 |
3 | runtime.install({
4 | onUpdateReady() {
5 | runtime.applyUpdate();
6 | },
7 | onUpdated() {
8 | location.reload(); // eslint-disable-line no-restricted-globals
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/src/services/CloudWatchService.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import DatasetArray from './DatasetArray';
3 |
4 | export default class CloudWatchService {
5 | constructor({ periodMinutes = 5, backfillMinutes = 120, refreshMinutes = 5 } = {}, metrics) {
6 | this.metrics = metrics;
7 | this.periodMinutes = periodMinutes;
8 | this.backfillMinutes = backfillMinutes;
9 | this.refreshMinutes = refreshMinutes;
10 | this.maxDatapoints = Math.ceil(backfillMinutes / periodMinutes);
11 | this.updatedAt = null;
12 | this.datasets = new DatasetArray();
13 | this.start();
14 | this.tags = {};
15 | }
16 |
17 | start() {
18 | this.update();
19 | this.task = setInterval(this.update.bind(this), this.refreshMinutes * 60000);
20 | }
21 |
22 | stop() {
23 | clearInterval(this.task);
24 | }
25 |
26 | async update() {
27 | const options = {
28 | url: process.env.API_ENDPOINT,
29 | method: 'post',
30 | headers: {
31 | 'X-Api-Key': process.env.API_KEY,
32 | },
33 | data: {
34 | start: this.updatedAt || new Date(Date.now() - (this.backfillMinutes * 60 * 1000)),
35 | periodMinutes: this.periodMinutes,
36 | metrics: this.metrics,
37 | },
38 | };
39 | try {
40 | const response = await Vue.axios.request(options);
41 | this.updatedAt = new Date(); // Doesn't reach if request failed
42 | this.appendData(response.data);
43 | } catch (error) { } // eslint-disable-line no-empty
44 | return this.data;
45 | }
46 |
47 | appendData(newDatasets) {
48 | newDatasets.forEach((newDataset) => {
49 | this.datasets.upsert(this.tagAndLabel(newDataset));
50 | });
51 | this.datasets.removeDataDuplicates(this.maxDatapoints);
52 | }
53 |
54 | tagAndLabel(dataset) {
55 | // Add tags and labels from metrics objects
56 | const metric = this.metrics.find(m => m.id === dataset.id);
57 | return typeof metric === 'undefined' ? dataset : Object.assign(dataset, {
58 | tags: metric.tags,
59 | label: metric.label,
60 | secondaryAxis: metric.secondaryAxis || false,
61 | });
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/services/DatasetArray.js:
--------------------------------------------------------------------------------
1 | import objectAssignDeep from 'object-assign-deep';
2 |
3 | export default class DatasetArray {
4 | constructor() {
5 | this.array = [];
6 | }
7 |
8 | asArray() {
9 | return this.array;
10 | }
11 |
12 | upsert(newDataset) {
13 | const dataset = this.array.find(d => d.id === newDataset.id);
14 | if (typeof dataset === 'undefined') {
15 | this.array.push(newDataset); // Insert
16 | return;
17 | }
18 | dataset.data.push(...newDataset.data); // Update
19 | }
20 |
21 | removeDataDuplicates(maxDatapoints = Infinity) {
22 | this.array.forEach((dataset, index) => {
23 | this.array[index].data = dataset.data
24 | .filter((d, i, array) => array.map(a => a.t).lastIndexOf(d.t) === i)
25 | .slice(maxDatapoints * -1); // Ensure moving window
26 | });
27 | }
28 |
29 | // Math methods
30 |
31 | average() {
32 | return this.sum() / this.array.reduce((sum, dataset) => (
33 | sum + dataset.data.length
34 | ), 0);
35 | }
36 |
37 | max() {
38 | const max = Math.max(...this.array.reduce((merged, dataset) => (
39 | merged.concat(dataset.data.map(d => d.y))
40 | ), []));
41 | return max === -Infinity ? 0 : max;
42 | }
43 |
44 | sum() {
45 | return this.array.reduce((sum, dataset) => (
46 | sum + dataset.data.reduce((s, d) => s + d.y, 0)
47 | ), 0);
48 | }
49 |
50 | // Subset methods
51 |
52 | noZeros() {
53 | const datasets = new DatasetArray();
54 | this.array.forEach((dataset) => {
55 | const trimmedDataset = objectAssignDeep({}, dataset);
56 | trimmedDataset.data = trimmedDataset.data.filter(d => d.y > 0);
57 | datasets.array.push(trimmedDataset);
58 | });
59 | return datasets;
60 | }
61 |
62 | latest(count = 1) {
63 | const datasets = new DatasetArray();
64 | this.array.forEach((dataset) => {
65 | const trimmedDataset = objectAssignDeep({}, dataset);
66 | trimmedDataset.data = trimmedDataset.data.slice(count * -1);
67 | datasets.array.push(trimmedDataset);
68 | });
69 | return datasets;
70 | }
71 |
72 | tagged(tag) {
73 | const datasets = new DatasetArray();
74 | this.array.forEach((dataset) => {
75 | if (dataset.tags.includes(tag)) {
76 | datasets.array.push(dataset);
77 | }
78 | });
79 | return datasets;
80 | }
81 |
82 | ided(id) {
83 | const datasets = new DatasetArray();
84 | this.array.forEach((dataset) => {
85 | if (dataset.id === id) {
86 | datasets.array.push(dataset);
87 | }
88 | });
89 | return datasets;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/styles/vars.scss:
--------------------------------------------------------------------------------
1 | // Responsive
2 | $small: 900px;
3 | $large: 1800px;
4 |
5 | // Colors
6 | $secondary-font: rgba(255, 255, 255, 0.3);
7 | $background-color: #21243f;
8 | $tile-color: #3c4166;
9 | $turquoise: #1abc9c;
10 | $green: #2ecc71;
11 | $blue: #3498db;
12 | $purple: #9b59b6;
13 | $yellow: #f1c40f;
14 | $orange: #e67e22;
15 | $red: #e74c3c;
16 |
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ashtonmeuser/vue-cloudwatch-dashboard/4dcc2ededc1e9eca7654fd76387ed4442202ebd6/static/favicon.ico
--------------------------------------------------------------------------------
/static/fonts/XRXV3I6Li01BKofINeaBTMnFcQ.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ashtonmeuser/vue-cloudwatch-dashboard/4dcc2ededc1e9eca7654fd76387ed4442202ebd6/static/fonts/XRXV3I6Li01BKofINeaBTMnFcQ.woff2
--------------------------------------------------------------------------------
/static/fonts/XRXV3I6Li01BKofIO-aBTMnFcQIG.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ashtonmeuser/vue-cloudwatch-dashboard/4dcc2ededc1e9eca7654fd76387ed4442202ebd6/static/fonts/XRXV3I6Li01BKofIO-aBTMnFcQIG.woff2
--------------------------------------------------------------------------------
/static/fonts/XRXW3I6Li01BKofA6sKUYevIWzgPDA.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ashtonmeuser/vue-cloudwatch-dashboard/4dcc2ededc1e9eca7654fd76387ed4442202ebd6/static/fonts/XRXW3I6Li01BKofA6sKUYevIWzgPDA.woff2
--------------------------------------------------------------------------------
/static/fonts/XRXW3I6Li01BKofA6sKUb-vIWzgPDEtj.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ashtonmeuser/vue-cloudwatch-dashboard/4dcc2ededc1e9eca7654fd76387ed4442202ebd6/static/fonts/XRXW3I6Li01BKofA6sKUb-vIWzgPDEtj.woff2
--------------------------------------------------------------------------------
/static/fonts/XRXW3I6Li01BKofAtsGUYevIWzgPDA.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ashtonmeuser/vue-cloudwatch-dashboard/4dcc2ededc1e9eca7654fd76387ed4442202ebd6/static/fonts/XRXW3I6Li01BKofAtsGUYevIWzgPDA.woff2
--------------------------------------------------------------------------------
/static/fonts/XRXW3I6Li01BKofAtsGUb-vIWzgPDEtj.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ashtonmeuser/vue-cloudwatch-dashboard/4dcc2ededc1e9eca7654fd76387ed4442202ebd6/static/fonts/XRXW3I6Li01BKofAtsGUb-vIWzgPDEtj.woff2
--------------------------------------------------------------------------------
/static/fonts/nunito.css:
--------------------------------------------------------------------------------
1 | /* latin-ext */
2 | @font-face {
3 | font-family: 'Nunito';
4 | font-style: normal;
5 | font-weight: 400;
6 | src: local('Nunito Regular'), local('Nunito-Regular'), url(./XRXV3I6Li01BKofIO-aBTMnFcQIG.woff2) format('woff2');
7 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
8 | }
9 | /* latin */
10 | @font-face {
11 | font-family: 'Nunito';
12 | font-style: normal;
13 | font-weight: 400;
14 | src: local('Nunito Regular'), local('Nunito-Regular'), url(./XRXV3I6Li01BKofINeaBTMnFcQ.woff2) format('woff2');
15 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
16 | }
17 | /* latin-ext */
18 | @font-face {
19 | font-family: 'Nunito';
20 | font-style: normal;
21 | font-weight: 600;
22 | src: local('Nunito SemiBold'), local('Nunito-SemiBold'), url(./XRXW3I6Li01BKofA6sKUb-vIWzgPDEtj.woff2) format('woff2');
23 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
24 | }
25 | /* latin */
26 | @font-face {
27 | font-family: 'Nunito';
28 | font-style: normal;
29 | font-weight: 600;
30 | src: local('Nunito SemiBold'), local('Nunito-SemiBold'), url(./XRXW3I6Li01BKofA6sKUYevIWzgPDA.woff2) format('woff2');
31 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
32 | }
33 | @font-face {
34 | font-family: 'Nunito';
35 | font-style: normal;
36 | font-weight: 900;
37 | src: local('Nunito Black'), local('Nunito-Black'), url(./XRXW3I6Li01BKofAtsGUb-vIWzgPDEtj.woff2) format('woff2');
38 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
39 | }
40 | /* latin */
41 | @font-face {
42 | font-family: 'Nunito';
43 | font-style: normal;
44 | font-weight: 900;
45 | src: local('Nunito Black'), local('Nunito-Black'), url(./XRXW3I6Li01BKofAtsGUYevIWzgPDA.woff2) format('woff2');
46 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
47 | }
48 |
--------------------------------------------------------------------------------
/static/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ashtonmeuser/vue-cloudwatch-dashboard/4dcc2ededc1e9eca7654fd76387ed4442202ebd6/static/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/static/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ashtonmeuser/vue-cloudwatch-dashboard/4dcc2ededc1e9eca7654fd76387ed4442202ebd6/static/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/static/icons/ios-icon-120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ashtonmeuser/vue-cloudwatch-dashboard/4dcc2ededc1e9eca7654fd76387ed4442202ebd6/static/icons/ios-icon-120.png
--------------------------------------------------------------------------------
/static/icons/ios-icon-152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ashtonmeuser/vue-cloudwatch-dashboard/4dcc2ededc1e9eca7654fd76387ed4442202ebd6/static/icons/ios-icon-152.png
--------------------------------------------------------------------------------
/static/icons/ios-icon-167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ashtonmeuser/vue-cloudwatch-dashboard/4dcc2ededc1e9eca7654fd76387ed4442202ebd6/static/icons/ios-icon-167.png
--------------------------------------------------------------------------------
/static/icons/ios-icon-180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ashtonmeuser/vue-cloudwatch-dashboard/4dcc2ededc1e9eca7654fd76387ed4442202ebd6/static/icons/ios-icon-180.png
--------------------------------------------------------------------------------
/static/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "CloudWatch Dashboard",
3 | "short_name": "Dashboard",
4 | "start_url": "./",
5 | "display": "standalone",
6 | "orientation": "portrait",
7 | "background_color": "#21243f",
8 | "theme_color": "#21243f",
9 | "icons": [
10 | {
11 | "src": "./icons/android-chrome-192x192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "./icons/android-chrome-512x512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/static/style.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | background-color: #21243f;
4 | margin: 0;
5 | height: 100%;
6 | }
7 |
--------------------------------------------------------------------------------