@electrolux-oss/plugin-infrawallet 0.1.0 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -10
- package/dist/api/InfraWalletApi.esm.js.map +1 -1
- package/dist/api/InfraWalletApiClient.esm.js +12 -65
- package/dist/api/InfraWalletApiClient.esm.js.map +1 -1
- package/dist/api/functions.esm.js +35 -28
- package/dist/api/functions.esm.js.map +1 -1
- package/dist/components/ColumnsChartComponent/ColumnsChartComponent.esm.js +44 -22
- package/dist/components/ColumnsChartComponent/ColumnsChartComponent.esm.js.map +1 -1
- package/dist/components/CostReportsTableComponent/CostReportsTableComponent.esm.js +49 -19
- package/dist/components/CostReportsTableComponent/CostReportsTableComponent.esm.js.map +1 -1
- package/dist/components/CostReportsTableComponent/TrendBarComponent.esm.js +1 -6
- package/dist/components/CostReportsTableComponent/TrendBarComponent.esm.js.map +1 -1
- package/dist/components/ErrorsAlertComponent/ErrorsAlertComponent.esm.js +40 -0
- package/dist/components/ErrorsAlertComponent/ErrorsAlertComponent.esm.js.map +1 -0
- package/dist/components/InfraWalletIcon.esm.js +61 -0
- package/dist/components/InfraWalletIcon.esm.js.map +1 -0
- package/dist/components/PieChartComponent/PieChartComponent.esm.js +3 -15
- package/dist/components/PieChartComponent/PieChartComponent.esm.js.map +1 -1
- package/dist/components/ReportsComponent/ReportsComponent.esm.js +21 -39
- package/dist/components/ReportsComponent/ReportsComponent.esm.js.map +1 -1
- package/dist/components/TopbarComponent/TopbarComponent.esm.js +1 -25
- package/dist/components/TopbarComponent/TopbarComponent.esm.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.esm.js +1 -0
- package/dist/index.esm.js.map +1 -1
- package/dist/plugin.esm.js.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
- Swift response times with cached cost data, ensuring rapid access to financial insights fetched from cloud platforms
|
|
12
12
|
- Easy configuration and deployment as a Backstage plugin, both frontend and backend plugins are production-ready
|
|
13
13
|
|
|
14
|
-
\*
|
|
14
|
+
\*_The latest version supports AWS, Azure and GCP cost aggregation while the framework is designed to be extensible to support others. Feel free to contribute to the project._
|
|
15
15
|
|
|
16
16
|
## Getting started
|
|
17
17
|
|
|
@@ -67,6 +67,24 @@ backend:
|
|
|
67
67
|
clientSecret: <Client_secret_of_the_created_application>
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
#### GCP
|
|
71
|
+
|
|
72
|
+
InfraWallet relies on GCP Big Query to fetch cost data. This means that the billing data needs to be exported to a big query dataset, and a service account needs to be created for InfraWallet. The steps of exporting billing data to Big Query can be found [here](https://cloud.google.com/billing/docs/how-to/export-data-bigquery). Then, visit Google Cloud Console and navigate to the `IAM & Admin` section in the billing account. Click `Service Accounts`, and create a new service account. The service account needs to have `BigQuery Data Viewer` and `BigQuery Job User` roles. On the `Service Accounts` page, click the three dots (menu) in the `Actions` column for the newly created service account and select `Manage keys`. There click `Add key` -> `Create new key`, and use `JSON` as the format. Download the JSON key file and keep it safe.
|
|
73
|
+
|
|
74
|
+
After setting up the resources above, add the following configurations in `app-config.yaml`:
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
backend:
|
|
78
|
+
infraWallet:
|
|
79
|
+
integrations:
|
|
80
|
+
gcp:
|
|
81
|
+
- name: <unique_name_of_this_account>
|
|
82
|
+
keyFilePath: <path_to_your_json_key_file> # if you run it in a k8s pod, you may need to create a secret and mount it to the pod
|
|
83
|
+
projectId: <GCP_project_that_your_big_query_dataset_belongs_to>
|
|
84
|
+
datasetId: <big_query_dataset_id>
|
|
85
|
+
tableId: <big_query_table_id>
|
|
86
|
+
```
|
|
87
|
+
|
|
70
88
|
### Adjust Category Mappings if Needed
|
|
71
89
|
|
|
72
90
|
The category mappings are stored in the plugin's database. If there is no mapping found in the DB when initializing the plugin, the default mappings will be used. The default mappings can be found in the [plugins/infrawallet-backend/seeds/init.js](plugins/infrawallet-backend/seeds/init.js) file. You can adjust this seed file to fit your needs, or update the database directly later on.
|
|
@@ -145,17 +163,17 @@ backend:
|
|
|
145
163
|
|
|
146
164
|
4. add InfraWallet to the sidebar (optional)
|
|
147
165
|
|
|
148
|
-
modify `packages/app/src/
|
|
166
|
+
modify `packages/app/src/components/Root/Root.tsx` and add the following code
|
|
149
167
|
|
|
150
168
|
```ts
|
|
151
169
|
...
|
|
152
|
-
import
|
|
170
|
+
import { InfraWalletIcon } from '@electrolux-oss/plugin-infrawallet';
|
|
153
171
|
...
|
|
154
172
|
<Sidebar>
|
|
155
173
|
...
|
|
156
174
|
<SidebarGroup label="Menu" icon={<MenuIcon />}>
|
|
157
175
|
<SidebarItem
|
|
158
|
-
icon={
|
|
176
|
+
icon={InfraWalletIcon}
|
|
159
177
|
to="infrawallet"
|
|
160
178
|
text="InfraWallet"
|
|
161
179
|
/>
|
|
@@ -180,13 +198,11 @@ import { createRouter } from '@electrolux-oss/plugin-infrawallet-backend';
|
|
|
180
198
|
import { Router } from 'express';
|
|
181
199
|
import { PluginEnvironment } from '../types';
|
|
182
200
|
|
|
183
|
-
export default async function createPlugin(
|
|
184
|
-
env: PluginEnvironment,
|
|
185
|
-
): Promise<Router> {
|
|
201
|
+
export default async function createPlugin(env: PluginEnvironment): Promise<Router> {
|
|
186
202
|
return await createRouter({
|
|
187
203
|
logger: env.logger,
|
|
188
204
|
config: env.config,
|
|
189
|
-
|
|
205
|
+
cache: env.cache.getClient(),
|
|
190
206
|
database: env.database,
|
|
191
207
|
});
|
|
192
208
|
}
|
|
@@ -209,9 +225,9 @@ async function main() {
|
|
|
209
225
|
|
|
210
226
|
## Local Development
|
|
211
227
|
|
|
212
|
-
Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn
|
|
228
|
+
First of all, make sure you are using either Node 18 or Node 20 for this project. Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn install && yarn dev` in the root directory, and then navigating to [/infrawallet](http://localhost:3000/infrawallet).
|
|
213
229
|
|
|
214
|
-
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
|
|
230
|
+
You can also serve the plugin in isolation by running `yarn install && yarn start` in the plugin directory.
|
|
215
231
|
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
|
|
216
232
|
It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.
|
|
217
233
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InfraWalletApi.esm.js","sources":["../../src/api/InfraWalletApi.ts"],"sourcesContent":["import { createApiRef } from '@backstage/core-plugin-api';\nimport { CostReportsResponse } from './types';\nimport { Response } from 'node-fetch';\n\n/** @public */\nexport const infraWalletApiRef = createApiRef<InfraWalletApi>({\n id: 'plugin.infrawallet',\n});\n\n/** @public */\nexport interface InfraWalletApi {\n get(path: string
|
|
1
|
+
{"version":3,"file":"InfraWalletApi.esm.js","sources":["../../src/api/InfraWalletApi.ts"],"sourcesContent":["import { createApiRef } from '@backstage/core-plugin-api';\nimport { CostReportsResponse } from './types';\nimport { Response } from 'node-fetch';\n\n/** @public */\nexport const infraWalletApiRef = createApiRef<InfraWalletApi>({\n id: 'plugin.infrawallet',\n});\n\n/** @public */\nexport interface InfraWalletApi {\n get(path: string): Promise<Response>;\n getCostReports(\n filters: string,\n groups: string,\n granularity: string,\n startTime: Date,\n endTime: Date,\n ): Promise<CostReportsResponse>;\n}\n"],"names":[],"mappings":";;AAKO,MAAM,oBAAoB,YAA6B,CAAA;AAAA,EAC5D,EAAI,EAAA,oBAAA;AACN,CAAC;;;;"}
|
|
@@ -13,75 +13,22 @@ class InfraWalletApiClient {
|
|
|
13
13
|
this.identityApi = options.identityApi;
|
|
14
14
|
this.backendUrl = options.configApi.getString("backend.baseUrl");
|
|
15
15
|
}
|
|
16
|
-
async get(path
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
`${this.backendUrl}/${path}`,
|
|
27
|
-
hdrs,
|
|
28
|
-
method,
|
|
29
|
-
data
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
async put(path, headers, data) {
|
|
33
|
-
const hdrs = {
|
|
34
|
-
...headers,
|
|
35
|
-
"Content-Type": "application/json"
|
|
36
|
-
};
|
|
37
|
-
const method = "PUT";
|
|
38
|
-
return await this.requestRaw(
|
|
39
|
-
`${this.backendUrl}/${path}`,
|
|
40
|
-
hdrs,
|
|
41
|
-
method,
|
|
42
|
-
data
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
async delete(path, headers, data) {
|
|
46
|
-
const hdrs = {
|
|
47
|
-
...headers,
|
|
48
|
-
"Content-Type": "application/json"
|
|
49
|
-
};
|
|
50
|
-
const method = "DELETE";
|
|
51
|
-
return await this.requestRaw(
|
|
52
|
-
`${this.backendUrl}/${path}`,
|
|
53
|
-
hdrs,
|
|
54
|
-
method,
|
|
55
|
-
data
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
async requestRaw(url, headers, method, data) {
|
|
59
|
-
let payload;
|
|
60
|
-
if (!method) {
|
|
61
|
-
payload = {
|
|
62
|
-
method: "GET",
|
|
63
|
-
headers
|
|
64
|
-
};
|
|
65
|
-
} else {
|
|
66
|
-
payload = {
|
|
67
|
-
method,
|
|
68
|
-
headers,
|
|
69
|
-
body: JSON.stringify(data)
|
|
70
|
-
};
|
|
16
|
+
async get(path) {
|
|
17
|
+
const url = `${this.backendUrl}/${path}`;
|
|
18
|
+
const { token: idToken } = await this.identityApi.getCredentials();
|
|
19
|
+
const response = await fetch(url, {
|
|
20
|
+
headers: idToken ? { Authorization: `Bearer ${idToken}` } : {}
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
const payload = await response.text();
|
|
24
|
+
const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
|
|
25
|
+
throw new Error(message);
|
|
71
26
|
}
|
|
72
|
-
return await
|
|
27
|
+
return await response.json();
|
|
73
28
|
}
|
|
74
29
|
async getCostReports(filters, groups, granularity, startTime, endTime) {
|
|
75
|
-
const { token: idToken } = await this.identityApi.getCredentials();
|
|
76
|
-
const headers = idToken ? { Authorization: `Bearer ${idToken}` } : {};
|
|
77
30
|
const url = `api/infrawallet/reports?&filters=${filters}&groups=${groups}&granularity=${granularity}&startTime=${startTime.getTime()}&endTime=${endTime.getTime()}`;
|
|
78
|
-
|
|
79
|
-
if (!response.ok) {
|
|
80
|
-
const r = await response.json();
|
|
81
|
-
throw new Error(r.error.message);
|
|
82
|
-
} else {
|
|
83
|
-
return await response.json();
|
|
84
|
-
}
|
|
31
|
+
return await this.get(url);
|
|
85
32
|
}
|
|
86
33
|
}
|
|
87
34
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InfraWalletApiClient.esm.js","sources":["../../src/api/InfraWalletApiClient.ts"],"sourcesContent":["import { ConfigApi, IdentityApi } from '@backstage/core-plugin-api';\nimport fetch
|
|
1
|
+
{"version":3,"file":"InfraWalletApiClient.esm.js","sources":["../../src/api/InfraWalletApiClient.ts"],"sourcesContent":["import { ConfigApi, IdentityApi } from '@backstage/core-plugin-api';\nimport fetch from 'node-fetch';\nimport { InfraWalletApi } from './InfraWalletApi';\nimport { CostReportsResponse } from './types';\n\n/** @public */\nexport class InfraWalletApiClient implements InfraWalletApi {\n private readonly identityApi: IdentityApi;\n private readonly backendUrl: string;\n\n constructor(options: { identityApi: IdentityApi; configApi: ConfigApi }) {\n this.identityApi = options.identityApi;\n this.backendUrl = options.configApi.getString('backend.baseUrl');\n }\n\n async get(path: string): Promise<any> {\n const url = `${this.backendUrl}/${path}`;\n const { token: idToken } = await this.identityApi.getCredentials();\n const response = await fetch(url, {\n headers: idToken ? { Authorization: `Bearer ${idToken}` } : {},\n });\n\n if (!response.ok) {\n const payload = await response.text();\n const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;\n throw new Error(message);\n }\n\n return await response.json();\n }\n\n async getCostReports(\n filters: string,\n groups: string,\n granularity: string,\n startTime: Date,\n endTime: Date,\n ): Promise<CostReportsResponse> {\n const url = `api/infrawallet/reports?&filters=${filters}&groups=${groups}&granularity=${granularity}&startTime=${startTime.getTime()}&endTime=${endTime.getTime()}`;\n return await this.get(url);\n }\n}\n"],"names":[],"mappings":";;;;;;;;AAMO,MAAM,oBAA+C,CAAA;AAAA,EAI1D,YAAY,OAA6D,EAAA;AAHzE,IAAiB,aAAA,CAAA,IAAA,EAAA,aAAA,CAAA,CAAA;AACjB,IAAiB,aAAA,CAAA,IAAA,EAAA,YAAA,CAAA,CAAA;AAGf,IAAA,IAAA,CAAK,cAAc,OAAQ,CAAA,WAAA,CAAA;AAC3B,IAAA,IAAA,CAAK,UAAa,GAAA,OAAA,CAAQ,SAAU,CAAA,SAAA,CAAU,iBAAiB,CAAA,CAAA;AAAA,GACjE;AAAA,EAEA,MAAM,IAAI,IAA4B,EAAA;AACpC,IAAA,MAAM,GAAM,GAAA,CAAA,EAAG,IAAK,CAAA,UAAU,IAAI,IAAI,CAAA,CAAA,CAAA;AACtC,IAAA,MAAM,EAAE,KAAO,EAAA,OAAA,KAAY,MAAM,IAAA,CAAK,YAAY,cAAe,EAAA,CAAA;AACjE,IAAM,MAAA,QAAA,GAAW,MAAM,KAAA,CAAM,GAAK,EAAA;AAAA,MAChC,OAAA,EAAS,UAAU,EAAE,aAAA,EAAe,UAAU,OAAO,CAAA,CAAA,KAAO,EAAC;AAAA,KAC9D,CAAA,CAAA;AAED,IAAI,IAAA,CAAC,SAAS,EAAI,EAAA;AAChB,MAAM,MAAA,OAAA,GAAU,MAAM,QAAA,CAAS,IAAK,EAAA,CAAA;AACpC,MAAM,MAAA,OAAA,GAAU,uBAAuB,QAAS,CAAA,MAAM,IAAI,QAAS,CAAA,UAAU,KAAK,OAAO,CAAA,CAAA,CAAA;AACzF,MAAM,MAAA,IAAI,MAAM,OAAO,CAAA,CAAA;AAAA,KACzB;AAEA,IAAO,OAAA,MAAM,SAAS,IAAK,EAAA,CAAA;AAAA,GAC7B;AAAA,EAEA,MAAM,cACJ,CAAA,OAAA,EACA,MACA,EAAA,WAAA,EACA,WACA,OAC8B,EAAA;AAC9B,IAAA,MAAM,GAAM,GAAA,CAAA,iCAAA,EAAoC,OAAO,CAAA,QAAA,EAAW,MAAM,CAAgB,aAAA,EAAA,WAAW,CAAc,WAAA,EAAA,SAAA,CAAU,OAAQ,EAAC,CAAY,SAAA,EAAA,OAAA,CAAQ,SAAS,CAAA,CAAA,CAAA;AACjK,IAAO,OAAA,MAAM,IAAK,CAAA,GAAA,CAAI,GAAG,CAAA,CAAA;AAAA,GAC3B;AACF;;;;"}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { reduce } from 'lodash';
|
|
2
1
|
import { parse, subMonths, format } from 'date-fns';
|
|
2
|
+
import { reduce } from 'lodash';
|
|
3
|
+
import moment from 'moment';
|
|
3
4
|
|
|
4
5
|
const mergeCostReports = (reports, threshold) => {
|
|
5
|
-
if (reports.length <= threshold) {
|
|
6
|
-
return reports;
|
|
7
|
-
}
|
|
8
6
|
const totalCosts = [];
|
|
9
7
|
reports.forEach((report) => {
|
|
10
8
|
let total = 0;
|
|
@@ -17,25 +15,25 @@ const mergeCostReports = (reports, threshold) => {
|
|
|
17
15
|
const idsToBeKept = sortedTotalCosts.slice(0, threshold).map((v) => v.id);
|
|
18
16
|
const mergedReports = reduce(
|
|
19
17
|
reports,
|
|
20
|
-
(
|
|
18
|
+
(accumulator, report) => {
|
|
21
19
|
let keyName = "others";
|
|
22
20
|
if (idsToBeKept.includes(report.id)) {
|
|
23
21
|
keyName = report.id;
|
|
24
22
|
}
|
|
25
|
-
if (!
|
|
26
|
-
|
|
23
|
+
if (!accumulator[keyName]) {
|
|
24
|
+
accumulator[keyName] = {
|
|
27
25
|
id: keyName,
|
|
28
26
|
reports: {}
|
|
29
27
|
};
|
|
30
28
|
}
|
|
31
29
|
Object.keys(report.reports).forEach((key) => {
|
|
32
|
-
if (
|
|
33
|
-
|
|
30
|
+
if (accumulator[keyName].reports[key]) {
|
|
31
|
+
accumulator[keyName].reports[key] += report.reports[key];
|
|
34
32
|
} else {
|
|
35
|
-
|
|
33
|
+
accumulator[keyName].reports[key] = report.reports[key];
|
|
36
34
|
}
|
|
37
35
|
});
|
|
38
|
-
return
|
|
36
|
+
return accumulator;
|
|
39
37
|
},
|
|
40
38
|
{}
|
|
41
39
|
);
|
|
@@ -44,28 +42,30 @@ const mergeCostReports = (reports, threshold) => {
|
|
|
44
42
|
const aggregateCostReports = (reports, aggregatedBy) => {
|
|
45
43
|
const aggregatedReports = reduce(
|
|
46
44
|
reports,
|
|
47
|
-
(
|
|
45
|
+
(accumulator, report) => {
|
|
48
46
|
let keyName = "no value";
|
|
49
47
|
if (aggregatedBy && aggregatedBy in report) {
|
|
50
48
|
keyName = report[aggregatedBy];
|
|
51
49
|
} else if (aggregatedBy === "none") {
|
|
52
50
|
keyName = "Total cloud costs";
|
|
53
51
|
}
|
|
54
|
-
if (!
|
|
55
|
-
|
|
52
|
+
if (!accumulator[keyName]) {
|
|
53
|
+
accumulator[keyName] = {
|
|
56
54
|
id: keyName,
|
|
57
55
|
reports: {}
|
|
58
56
|
};
|
|
59
|
-
|
|
57
|
+
if (aggregatedBy !== void 0) {
|
|
58
|
+
accumulator[keyName][aggregatedBy] = keyName;
|
|
59
|
+
}
|
|
60
60
|
}
|
|
61
61
|
Object.keys(report.reports).forEach((key) => {
|
|
62
|
-
if (
|
|
63
|
-
|
|
62
|
+
if (accumulator[keyName].reports[key]) {
|
|
63
|
+
accumulator[keyName].reports[key] += report.reports[key];
|
|
64
64
|
} else {
|
|
65
|
-
|
|
65
|
+
accumulator[keyName].reports[key] = report.reports[key];
|
|
66
66
|
}
|
|
67
67
|
});
|
|
68
|
-
return
|
|
68
|
+
return accumulator;
|
|
69
69
|
},
|
|
70
70
|
{}
|
|
71
71
|
);
|
|
@@ -73,14 +73,7 @@ const aggregateCostReports = (reports, aggregatedBy) => {
|
|
|
73
73
|
};
|
|
74
74
|
const getAllReportTags = (reports) => {
|
|
75
75
|
const tags = /* @__PURE__ */ new Set();
|
|
76
|
-
const reservedKeys = [
|
|
77
|
-
"id",
|
|
78
|
-
"name",
|
|
79
|
-
"service",
|
|
80
|
-
"category",
|
|
81
|
-
"provider",
|
|
82
|
-
"reports"
|
|
83
|
-
];
|
|
76
|
+
const reservedKeys = ["id", "name", "service", "category", "provider", "reports"];
|
|
84
77
|
reports.forEach((report) => {
|
|
85
78
|
Object.keys(report).forEach((key) => {
|
|
86
79
|
if (reservedKeys.indexOf(key) === -1) {
|
|
@@ -95,6 +88,20 @@ const getPreviousMonth = (month) => {
|
|
|
95
88
|
const previousMonth = subMonths(date, 1);
|
|
96
89
|
return format(previousMonth, "yyyy-MM");
|
|
97
90
|
};
|
|
91
|
+
const getPeriodStrings = (granularity, startTime, endTime) => {
|
|
92
|
+
const result = [];
|
|
93
|
+
const current = moment(startTime);
|
|
94
|
+
while (current.isSameOrBefore(endTime) && current.isSameOrBefore(moment())) {
|
|
95
|
+
if (granularity === "monthly") {
|
|
96
|
+
result.push(current.format("YYYY-MM"));
|
|
97
|
+
current.add(1, "months");
|
|
98
|
+
} else {
|
|
99
|
+
result.push(current.format("YYYY-MM-DD"));
|
|
100
|
+
current.add(1, "days");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
};
|
|
98
105
|
|
|
99
|
-
export { aggregateCostReports, getAllReportTags, getPreviousMonth, mergeCostReports };
|
|
106
|
+
export { aggregateCostReports, getAllReportTags, getPeriodStrings, getPreviousMonth, mergeCostReports };
|
|
100
107
|
//# sourceMappingURL=functions.esm.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"functions.esm.js","sources":["../../src/api/functions.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"functions.esm.js","sources":["../../src/api/functions.ts"],"sourcesContent":["import { format, parse, subMonths } from 'date-fns';\nimport { reduce } from 'lodash';\nimport moment from 'moment';\nimport { Report } from './types';\n\nexport const mergeCostReports = (reports: Report[], threshold: number): Report[] => {\n const totalCosts: { id: string; total: number }[] = [];\n reports.forEach(report => {\n let total = 0;\n Object.values(report.reports).forEach(v => {\n total += v as number;\n });\n totalCosts.push({ id: report.id, total: total });\n });\n const sortedTotalCosts = totalCosts.sort((a, b) => b.total - a.total);\n const idsToBeKept = sortedTotalCosts.slice(0, threshold).map(v => v.id);\n\n const mergedReports = reduce(\n reports,\n (accumulator: { [key: string]: Report }, report) => {\n let keyName = 'others';\n if (idsToBeKept.includes(report.id)) {\n keyName = report.id;\n }\n if (!accumulator[keyName]) {\n accumulator[keyName] = {\n id: keyName,\n reports: {},\n };\n }\n\n Object.keys(report.reports).forEach(key => {\n if (accumulator[keyName].reports[key]) {\n accumulator[keyName].reports[key] += report.reports[key];\n } else {\n accumulator[keyName].reports[key] = report.reports[key];\n }\n });\n return accumulator;\n },\n {},\n );\n\n return Object.values(mergedReports);\n};\n\nexport const aggregateCostReports = (reports: Report[], aggregatedBy?: string): Report[] => {\n const aggregatedReports: { [key: string]: Report } = reduce(\n reports,\n (accumulator, report) => {\n let keyName: string = 'no value';\n if (aggregatedBy && aggregatedBy in report) {\n keyName = report[aggregatedBy] as string;\n } else if (aggregatedBy === 'none') {\n keyName = 'Total cloud costs';\n }\n\n if (!accumulator[keyName]) {\n accumulator[keyName] = {\n id: keyName,\n reports: {},\n } as {\n id: string;\n reports: { [key: string]: number };\n [key: string]: any;\n };\n\n if (aggregatedBy !== undefined) {\n accumulator[keyName][aggregatedBy] = keyName;\n }\n }\n\n Object.keys(report.reports).forEach(key => {\n if (accumulator[keyName].reports[key]) {\n accumulator[keyName].reports[key] += report.reports[key];\n } else {\n accumulator[keyName].reports[key] = report.reports[key];\n }\n });\n return accumulator;\n },\n {} as { [key: string]: Report },\n );\n return Object.values(aggregatedReports);\n};\n\nexport const getAllReportTags = (reports: Report[]): string[] => {\n const tags = new Set<string>();\n const reservedKeys = ['id', 'name', 'service', 'category', 'provider', 'reports'];\n reports.forEach(report => {\n Object.keys(report).forEach(key => {\n if (reservedKeys.indexOf(key) === -1) {\n tags.add(key);\n }\n });\n });\n return Array.from(tags);\n};\n\nexport const getPreviousMonth = (month: string): string => {\n const date = parse(month, 'yyyy-MM', new Date());\n const previousMonth = subMonths(date, 1);\n return format(previousMonth, 'yyyy-MM');\n};\n\nexport const getPeriodStrings = (granularity: string, startTime: Date, endTime: Date): string[] => {\n const result: string[] = [];\n const current = moment(startTime);\n\n while (current.isSameOrBefore(endTime) && current.isSameOrBefore(moment())) {\n if (granularity === 'monthly') {\n result.push(current.format('YYYY-MM'));\n current.add(1, 'months');\n } else {\n result.push(current.format('YYYY-MM-DD'));\n current.add(1, 'days');\n }\n }\n\n return result;\n};\n"],"names":[],"mappings":"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"}
|
|
@@ -1,10 +1,44 @@
|
|
|
1
|
-
import { Paper } from '@material-ui/core';
|
|
2
|
-
import { useTheme, makeStyles } from '@material-ui/core/styles';
|
|
1
|
+
import { Switch, Paper, Grid } from '@material-ui/core';
|
|
2
|
+
import { withStyles, useTheme, makeStyles } from '@material-ui/core/styles';
|
|
3
3
|
import humanFormat from 'human-format';
|
|
4
4
|
import React from 'react';
|
|
5
5
|
import Chart from 'react-apexcharts';
|
|
6
6
|
|
|
7
|
+
const Toggle = withStyles((theme) => ({
|
|
8
|
+
root: {
|
|
9
|
+
width: 28,
|
|
10
|
+
height: 16,
|
|
11
|
+
padding: 0,
|
|
12
|
+
display: "flex"
|
|
13
|
+
},
|
|
14
|
+
switchBase: {
|
|
15
|
+
padding: 2,
|
|
16
|
+
color: theme.palette.grey[500],
|
|
17
|
+
"&$checked": {
|
|
18
|
+
transform: "translateX(12px)",
|
|
19
|
+
color: theme.palette.common.white,
|
|
20
|
+
"& + $track": {
|
|
21
|
+
opacity: 1,
|
|
22
|
+
backgroundColor: theme.palette.primary.main,
|
|
23
|
+
borderColor: theme.palette.primary.main
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
thumb: {
|
|
28
|
+
width: 12,
|
|
29
|
+
height: 12,
|
|
30
|
+
boxShadow: "none"
|
|
31
|
+
},
|
|
32
|
+
track: {
|
|
33
|
+
border: `1px solid ${theme.palette.grey[500]}`,
|
|
34
|
+
borderRadius: 16 / 2,
|
|
35
|
+
opacity: 1,
|
|
36
|
+
backgroundColor: theme.palette.common.white
|
|
37
|
+
},
|
|
38
|
+
checked: {}
|
|
39
|
+
}))(Switch);
|
|
7
40
|
const ColumnsChartComponent = ({
|
|
41
|
+
granularitySetter,
|
|
8
42
|
categories,
|
|
9
43
|
series,
|
|
10
44
|
height,
|
|
@@ -62,7 +96,7 @@ const ColumnsChartComponent = ({
|
|
|
62
96
|
},
|
|
63
97
|
stacked: true,
|
|
64
98
|
toolbar: {
|
|
65
|
-
show:
|
|
99
|
+
show: false
|
|
66
100
|
},
|
|
67
101
|
events: {
|
|
68
102
|
dataPointSelection: dataPointSelectionHandler
|
|
@@ -75,14 +109,16 @@ const ColumnsChartComponent = ({
|
|
|
75
109
|
decimalsInFloat: 2
|
|
76
110
|
},
|
|
77
111
|
dataLabels: {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
112
|
+
enabled: false
|
|
113
|
+
},
|
|
114
|
+
tooltip: {
|
|
115
|
+
y: {
|
|
116
|
+
formatter: (value) => {
|
|
117
|
+
return `$${humanFormat(value, {
|
|
81
118
|
scale: customScale,
|
|
82
119
|
separator: ""
|
|
83
120
|
})}`;
|
|
84
121
|
}
|
|
85
|
-
return "null";
|
|
86
122
|
}
|
|
87
123
|
},
|
|
88
124
|
legend: {
|
|
@@ -147,21 +183,7 @@ const ColumnsChartComponent = ({
|
|
|
147
183
|
},
|
|
148
184
|
series
|
|
149
185
|
};
|
|
150
|
-
return /* @__PURE__ */ React.createElement(
|
|
151
|
-
Paper,
|
|
152
|
-
{
|
|
153
|
-
className: thumbnail ? classes.thumbnailPaper : classes.fixedHeightPaper
|
|
154
|
-
},
|
|
155
|
-
/* @__PURE__ */ React.createElement(
|
|
156
|
-
Chart,
|
|
157
|
-
{
|
|
158
|
-
options: state.options,
|
|
159
|
-
series: state.series,
|
|
160
|
-
type: "bar",
|
|
161
|
-
height: height ? height - 50 : 250
|
|
162
|
-
}
|
|
163
|
-
)
|
|
164
|
-
);
|
|
186
|
+
return /* @__PURE__ */ React.createElement(Paper, { className: thumbnail ? classes.thumbnailPaper : classes.fixedHeightPaper }, /* @__PURE__ */ React.createElement(Grid, { container: true, justifyContent: "flex-end", spacing: 1 }, /* @__PURE__ */ React.createElement(Grid, { item: true }, "Monthly"), /* @__PURE__ */ React.createElement(Grid, { item: true }, /* @__PURE__ */ React.createElement(Toggle, { onChange: (event) => granularitySetter(event.target.checked ? "daily" : "monthly") })), /* @__PURE__ */ React.createElement(Grid, { item: true }, "Daily")), /* @__PURE__ */ React.createElement(Chart, { options: state.options, series: state.series, type: "bar", height: height ? height - 50 : 250 }));
|
|
165
187
|
};
|
|
166
188
|
|
|
167
189
|
export { ColumnsChartComponent };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ColumnsChartComponent.esm.js","sources":["../../../src/components/ColumnsChartComponent/ColumnsChartComponent.tsx"],"sourcesContent":["import { Paper } from '@material-ui/core';\nimport { makeStyles, useTheme } from '@material-ui/core/styles';\nimport humanFormat from 'human-format';\nimport React, { FC } from 'react';\nimport Chart from 'react-apexcharts';\nimport { ColumnsChartComponentProps } from '../types';\n\nexport const ColumnsChartComponent: FC<ColumnsChartComponentProps> = ({\n categories,\n series,\n height,\n thumbnail,\n dataPointSelectionHandler,\n}) => {\n const defaultTheme = useTheme();\n const useStyles = makeStyles({\n fixedHeightPaper: {\n padding: '16px',\n display: 'flex',\n overflow: 'auto',\n flexDirection: 'column',\n height: height ? height : 300,\n },\n thumbnailPaper: {\n display: 'flex',\n overflow: 'auto',\n flexDirection: 'column',\n height: height ? height - 50 : 100,\n },\n });\n const classes = useStyles();\n const customScale = humanFormat.Scale.create(['', 'K', 'M', 'B'], 1000);\n\n const state = thumbnail\n ? {\n options: {\n chart: {\n animations: {\n enabled: false,\n },\n zoom: {\n enabled: false,\n },\n stacked: true,\n toolbar: {\n show: false,\n },\n sparkline: {\n enabled: true,\n },\n },\n xaxis: {\n categories: categories,\n },\n theme: {\n mode: defaultTheme.palette.type,\n },\n },\n series: series,\n }\n : {\n options: {\n chart: {\n animations: {\n enabled: false,\n },\n stacked: true,\n toolbar: {\n show:
|
|
1
|
+
{"version":3,"file":"ColumnsChartComponent.esm.js","sources":["../../../src/components/ColumnsChartComponent/ColumnsChartComponent.tsx"],"sourcesContent":["import { Grid, Paper, Switch } from '@material-ui/core';\nimport { withStyles, makeStyles, useTheme } from '@material-ui/core/styles';\nimport humanFormat from 'human-format';\nimport React, { FC } from 'react';\nimport Chart from 'react-apexcharts';\nimport { ColumnsChartComponentProps } from '../types';\n\nconst Toggle = withStyles(theme => ({\n root: {\n width: 28,\n height: 16,\n padding: 0,\n display: 'flex',\n },\n switchBase: {\n padding: 2,\n color: theme.palette.grey[500],\n '&$checked': {\n transform: 'translateX(12px)',\n color: theme.palette.common.white,\n '& + $track': {\n opacity: 1,\n backgroundColor: theme.palette.primary.main,\n borderColor: theme.palette.primary.main,\n },\n },\n },\n thumb: {\n width: 12,\n height: 12,\n boxShadow: 'none',\n },\n track: {\n border: `1px solid ${theme.palette.grey[500]}`,\n borderRadius: 16 / 2,\n opacity: 1,\n backgroundColor: theme.palette.common.white,\n },\n checked: {},\n}))(Switch);\n\nexport const ColumnsChartComponent: FC<ColumnsChartComponentProps> = ({\n granularitySetter,\n categories,\n series,\n height,\n thumbnail,\n dataPointSelectionHandler,\n}) => {\n const defaultTheme = useTheme();\n const useStyles = makeStyles({\n fixedHeightPaper: {\n padding: '16px',\n display: 'flex',\n overflow: 'auto',\n flexDirection: 'column',\n height: height ? height : 300,\n },\n thumbnailPaper: {\n display: 'flex',\n overflow: 'auto',\n flexDirection: 'column',\n height: height ? height - 50 : 100,\n },\n });\n const classes = useStyles();\n const customScale = humanFormat.Scale.create(['', 'K', 'M', 'B'], 1000);\n\n const state = thumbnail\n ? {\n options: {\n chart: {\n animations: {\n enabled: false,\n },\n zoom: {\n enabled: false,\n },\n stacked: true,\n toolbar: {\n show: false,\n },\n sparkline: {\n enabled: true,\n },\n },\n xaxis: {\n categories: categories,\n },\n theme: {\n mode: defaultTheme.palette.type,\n },\n },\n series: series,\n }\n : {\n options: {\n chart: {\n animations: {\n enabled: false,\n },\n stacked: true,\n toolbar: {\n show: false,\n },\n events: {\n dataPointSelection: dataPointSelectionHandler,\n },\n },\n xaxis: {\n categories: categories,\n },\n yaxis: {\n decimalsInFloat: 2,\n },\n dataLabels: {\n enabled: false,\n },\n tooltip: {\n y: {\n formatter: (value: number) => {\n return `$${humanFormat(value, {\n scale: customScale,\n separator: '',\n })}`;\n },\n },\n },\n legend: {\n showForSingleSeries: true,\n },\n theme: {\n mode: defaultTheme.palette.type,\n },\n // there are only 5 colors by default, here we extend it to 50 different colors\n colors: [\n '#008FFB',\n '#00E396',\n '#FEB019',\n '#FF4560',\n '#775DD0',\n '#3F51B5',\n '#03A9F4',\n '#4CAF50',\n '#F9CE1D',\n '#FF9800',\n '#33B2DF',\n '#546E7A',\n '#D4526E',\n '#13D8AA',\n '#A5978B',\n '#4ECDC4',\n '#C7F464',\n '#81D4FA',\n '#546E7A',\n '#FD6A6A',\n '#2B908F',\n '#F9A3A4',\n '#90EE7E',\n '#FA4443',\n '#69D2E7',\n '#449DD1',\n '#F86624',\n '#EA3546',\n '#662E9B',\n '#C5D86D',\n '#D7263D',\n '#1B998B',\n '#2E294E',\n '#F46036',\n '#E2C044',\n '#662E9B',\n '#F86624',\n '#F9C80E',\n '#EA3546',\n '#43BCCD',\n '#5C4742',\n '#A5978B',\n '#8D5B4C',\n '#5A2A27',\n '#C4BBAF',\n '#A300D6',\n '#7D02EB',\n '#5653FE',\n '#2983FF',\n '#00B1F2',\n ],\n },\n series: series,\n };\n\n return (\n <Paper className={thumbnail ? classes.thumbnailPaper : classes.fixedHeightPaper}>\n <Grid container justifyContent=\"flex-end\" spacing={1}>\n <Grid item>Monthly</Grid>\n <Grid item>\n <Toggle onChange={event => granularitySetter(event.target.checked ? 'daily' : 'monthly')} />\n </Grid>\n <Grid item>Daily</Grid>\n </Grid>\n <Chart options={state.options} series={state.series} type=\"bar\" height={height ? height - 50 : 250} />\n </Paper>\n );\n};\n"],"names":[],"mappings":"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"}
|