@electrolux-oss/plugin-infrawallet 0.1.0

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.
Files changed (27) hide show
  1. package/README.md +258 -0
  2. package/dist/api/InfraWalletApi.esm.js +8 -0
  3. package/dist/api/InfraWalletApi.esm.js.map +1 -0
  4. package/dist/api/InfraWalletApiClient.esm.js +89 -0
  5. package/dist/api/InfraWalletApiClient.esm.js.map +1 -0
  6. package/dist/api/functions.esm.js +100 -0
  7. package/dist/api/functions.esm.js.map +1 -0
  8. package/dist/components/ColumnsChartComponent/ColumnsChartComponent.esm.js +168 -0
  9. package/dist/components/ColumnsChartComponent/ColumnsChartComponent.esm.js.map +1 -0
  10. package/dist/components/CostReportsTableComponent/CostReportsTableComponent.esm.js +144 -0
  11. package/dist/components/CostReportsTableComponent/CostReportsTableComponent.esm.js.map +1 -0
  12. package/dist/components/CostReportsTableComponent/TrendBarComponent.esm.js +48 -0
  13. package/dist/components/CostReportsTableComponent/TrendBarComponent.esm.js.map +1 -0
  14. package/dist/components/PieChartComponent/PieChartComponent.esm.js +134 -0
  15. package/dist/components/PieChartComponent/PieChartComponent.esm.js.map +1 -0
  16. package/dist/components/ReportsComponent/ReportsComponent.esm.js +126 -0
  17. package/dist/components/ReportsComponent/ReportsComponent.esm.js.map +1 -0
  18. package/dist/components/TopbarComponent/TopbarComponent.esm.js +93 -0
  19. package/dist/components/TopbarComponent/TopbarComponent.esm.js.map +1 -0
  20. package/dist/index.d.ts +10 -0
  21. package/dist/index.esm.js +2 -0
  22. package/dist/index.esm.js.map +1 -0
  23. package/dist/plugin.esm.js +28 -0
  24. package/dist/plugin.esm.js.map +1 -0
  25. package/dist/routes.esm.js +12 -0
  26. package/dist/routes.esm.js.map +1 -0
  27. package/package.json +78 -0
package/README.md ADDED
@@ -0,0 +1,258 @@
1
+ # InfraWallet
2
+
3
+ > Control your cloud costs just in the way how you control your bank accounts
4
+
5
+ ![InfraWallet](./docs/images/iw_demo.gif)
6
+
7
+ ## Highlights
8
+
9
+ - Flexible aggregation options for cloud costs across multiple platforms and accounts\*
10
+ - Cost categorization for aggregating expenses across different cloud vendors with configurable category mappings
11
+ - Swift response times with cached cost data, ensuring rapid access to financial insights fetched from cloud platforms
12
+ - Easy configuration and deployment as a Backstage plugin, both frontend and backend plugins are production-ready
13
+
14
+ \*_This version prioritizes AWS and Azure as the primary cloud vendors, but the framework is designed to be extensible to support others. Feel free to contribute to the project or wait for the next version with expanded cloud vendor support._
15
+
16
+ ## Getting started
17
+
18
+ ### Define Cloud Accounts in app-config.yaml
19
+
20
+ The configuration schema of InfraWallet is defined in the [plugins/infrawallet-backend/config.d.ts](plugins/infrawallet-backend/config.d.ts) file. Users need to configure their cloud accounts in the `app-config.yaml` in the root folder.
21
+
22
+ #### AWS
23
+
24
+ For AWS, InfraWallet relies on an IAM role to fetch cost and usage data using AWS Cost Explorer APIs. Thus before adding the configurations, AWS IAM user, role, and policy need to be set up. If you have multiple AWS accounts, you can reuse the IAM user in one account and grant the necessary permissions to the role in each account. The role to be assumed in an AWS account needs the following permission:
25
+
26
+ ```json
27
+ {
28
+ "Statement": [
29
+ {
30
+ "Action": "ce:GetCostAndUsage",
31
+ "Effect": "Allow",
32
+ "Resource": "*",
33
+ "Sid": ""
34
+ }
35
+ ],
36
+ "Version": "2012-10-17"
37
+ }
38
+ ```
39
+
40
+ After getting the IAM-related resources ready, put the following configuration into `app-config.yaml`:
41
+
42
+ ```yaml
43
+ backend:
44
+ infraWallet:
45
+ integrations:
46
+ aws:
47
+ - name: <unique_name_of_this_account>
48
+ accountId: '<12-digit_account_ID>' # quoted as a string
49
+ assumedRoleName: <name_of_the_AWS_IAM_role_to_be_assumed>
50
+ accessKeyId: <access_key_ID_of_AWS_IAM_user_that_assumes_the_role>
51
+ accessKeySecret: <access_key_secret_of_AWS_IAM_user_that_assumes_the_role>
52
+ ```
53
+
54
+ #### Azure
55
+
56
+ In order to manage Azure costs, an application needs to be registered on Azure. InfraWallet is only tested with subscription-level cost data. After creating the application, users need to go to the `Subscriptions` page, choose the target subscription and then visit the `Access control (IAM)` page. Assign the `Cost Management Reader` role to the created application. Create a new client secret for the application, and add the following configurations in `app-config.yaml`:
57
+
58
+ ```yaml
59
+ backend:
60
+ infraWallet:
61
+ integrations:
62
+ azure:
63
+ - name: <unique_name_of_this_account>
64
+ subscriptionId: <Azure_subscription_ID>
65
+ tenantId: <Azure_tenant_ID>
66
+ clientId: <Client_ID_of_the_created_application>
67
+ clientSecret: <Client_secret_of_the_created_application>
68
+ ```
69
+
70
+ ### Adjust Category Mappings if Needed
71
+
72
+ 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.
73
+
74
+ ### Install the Plugin
75
+
76
+ #### If Backstage New Backend System is enabled
77
+
78
+ 1. add InfraWallet frontend
79
+
80
+ ```
81
+ # From your Backstage root directory
82
+ yarn --cwd packages/app add @electrolux-oss/plugin-infrawallet
83
+ ```
84
+
85
+ modify `packages/app/src/App.tsx` and add the following code
86
+
87
+ ```ts
88
+ ...
89
+ import { InfraWalletPage } from '@electrolux-oss/plugin-infrawallet';
90
+ ...
91
+ <FlatRoutes>
92
+ ...
93
+ <Route path="/infrawallet" element={<InfraWalletPage />} />
94
+ </FlatRoutes>
95
+ ...
96
+ ```
97
+
98
+ 2. add InfraWallet backend
99
+
100
+ ```
101
+ # From your Backstage root directory
102
+ yarn --cwd packages/backend add @electrolux-oss/plugin-infrawallet-backend
103
+ ```
104
+
105
+ modify `packages/backend/src/index.ts` and add the following code before `backend.start()`;
106
+
107
+ ```typescript
108
+ ...
109
+ // InfraWallet backend
110
+ backend.add(import('@electrolux-oss/plugin-infrawallet-backend'));
111
+ ...
112
+ backend.start();
113
+ ```
114
+
115
+ 3. add cloud account credentials to `app-config.yaml`
116
+ Here is an example of the configuration for AWS and Azure accounts:
117
+
118
+ ```yaml
119
+ backend:
120
+ infraWallet:
121
+ integrations:
122
+ azure:
123
+ - name: <unique_name_of_this_account>
124
+ subscriptionId: ...
125
+ tenantId: ...
126
+ clientId: ...
127
+ clientSecret: ...
128
+ - name: <unique_name_of_this_account>
129
+ subscriptionId: ...
130
+ tenantId: ...
131
+ clientId: ...
132
+ clientSecret: ...
133
+ aws:
134
+ - name: <unique_name_of_this_account>
135
+ accountId: '<12-digit_account_ID_as_string>'
136
+ assumedRoleName: ...
137
+ accessKeyId: ...
138
+ accessKeySecret: ...
139
+ - name: <unique_name_of_this_account>
140
+ accountId: '<12-digit_account_ID_as_string>'
141
+ assumedRoleName: ...
142
+ accessKeyId: ...
143
+ accessKeySecret: ...
144
+ ```
145
+
146
+ 4. add InfraWallet to the sidebar (optional)
147
+
148
+ modify `packages/app/src/App.tsx` and add the following code
149
+
150
+ ```ts
151
+ ...
152
+ import AccountBalanceWalletIcon from '@material-ui/icons/AccountBalanceWallet';
153
+ ...
154
+ <Sidebar>
155
+ ...
156
+ <SidebarGroup label="Menu" icon={<MenuIcon />}>
157
+ <SidebarItem
158
+ icon={AccountBalanceWalletIcon}
159
+ to="infrawallet"
160
+ text="InfraWallet"
161
+ />
162
+ </SidebarGroup>
163
+ ...
164
+ </Sidebar>
165
+ ```
166
+
167
+ #### If the legacy Backstage backend system is used
168
+
169
+ The 2nd step above (adding the backend) is different and it should be like the following.
170
+
171
+ ```
172
+ # From your Backstage root directory
173
+ yarn --cwd packages/backend add @electrolux-oss/plugin-infrawallet-backend
174
+ ```
175
+
176
+ create a file `infrawallet.ts` in folder `packages/backend/src/plugins/` with the following content.
177
+
178
+ ```ts
179
+ import { createRouter } from '@electrolux-oss/plugin-infrawallet-backend';
180
+ import { Router } from 'express';
181
+ import { PluginEnvironment } from '../types';
182
+
183
+ export default async function createPlugin(
184
+ env: PluginEnvironment,
185
+ ): Promise<Router> {
186
+ return await createRouter({
187
+ logger: env.logger,
188
+ config: env.config,
189
+ audit: env.audit,
190
+ database: env.database,
191
+ });
192
+ }
193
+ ```
194
+
195
+ then modify `packages/backend/src/index.ts`
196
+
197
+ ```ts
198
+ ...
199
+ import infraWallet from './plugins/infrawallet';
200
+ ...
201
+ async function main() {
202
+ ...
203
+ const infraWalletEnv = useHotMemoize(module, () => createEnv('infrawallet'));
204
+ ...
205
+ apiRouter.use('/infrawallet', authMiddleware, await infraWallet(infraWalletEnv));
206
+ ...
207
+ }
208
+ ```
209
+
210
+ ## Local Development
211
+
212
+ Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/infrawallet](http://localhost:3000/infrawallet).
213
+
214
+ You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
215
+ This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
216
+ It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.
217
+
218
+ ## How to Support a New Cloud Vendor?
219
+
220
+ In InfraWallet, all the cost data fetched from different cloud providers are transformed into a generic format:
221
+
222
+ ```typescript
223
+ export type Report = {
224
+ id: string; // the unique ID of a cloud account which is defined in the app-config.yaml file
225
+ [dimension: string]: string | { [period: string]: number } | undefined; // other dimensions such as category, service, a tag, etc.
226
+ reports?: {
227
+ [period: string]: number; // the reports which are in the following format ["period": cost], such as ["2024-01": 12.23, "2024-02": 23.21]
228
+ };
229
+ };
230
+ ```
231
+
232
+ For example, here is a report returned from InfraWallet backend:
233
+
234
+ ```json
235
+ {
236
+ "id": "my-aws-dev-account",
237
+ "provider": "aws",
238
+ "category": "Infrastructure",
239
+ "service": "EC2",
240
+ "reports": {
241
+ "2024-01": 12.23,
242
+ "2024-02": 23.21
243
+ }
244
+ }
245
+ ```
246
+
247
+ The aggregation is done by the frontend after getting all the needed cost reports. This means that as long as the backend returns more cost reports in the same format, InfraWallet can always aggregate and visualize the costs.
248
+
249
+ When adding a new cloud vendor, you need to implement a client based on the interface [InfraWalletApi](plugins/infrawallet-backend/src/service/InfraWalletApi.ts). Check [AwsClient.ts](plugins/infrawallet-backend/src/service/AwsClient.ts) and [AzureClient.ts](plugins/infrawallet-backend/src/service/AzureClient.ts) as examples.
250
+
251
+ ## Roadmap
252
+
253
+ - [ ] Make IAM user optional for AWS credentials
254
+ - [ ] Support filters besides grouping bys
255
+ - [ ] Support Google Cloud Costs
256
+ - [ ] WebUI for managing category mappings
257
+ - [ ] Enable users to select a subset of configured cloud accounts as a wallet
258
+ - [ ] Support different currencies
@@ -0,0 +1,8 @@
1
+ import { createApiRef } from '@backstage/core-plugin-api';
2
+
3
+ const infraWalletApiRef = createApiRef({
4
+ id: "plugin.infrawallet"
5
+ });
6
+
7
+ export { infraWalletApiRef };
8
+ //# sourceMappingURL=InfraWalletApi.esm.js.map
@@ -0,0 +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, headers?: Record<string, string>): Promise<Response>;\n post(\n path: string,\n headers?: Record<string, string>,\n data?: Record<string, any | undefined>,\n ): Promise<Response>;\n put(\n path: string,\n headers?: Record<string, string>,\n data?: Record<string, any | undefined>,\n ): Promise<Response>;\n delete(\n path: string,\n headers?: Record<string, string>,\n data?: Record<string, any | undefined>,\n ): 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;;;;"}
@@ -0,0 +1,89 @@
1
+ import fetch from 'node-fetch';
2
+
3
+ var __defProp = Object.defineProperty;
4
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
5
+ var __publicField = (obj, key, value) => {
6
+ __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
7
+ return value;
8
+ };
9
+ class InfraWalletApiClient {
10
+ constructor(options) {
11
+ __publicField(this, "identityApi");
12
+ __publicField(this, "backendUrl");
13
+ this.identityApi = options.identityApi;
14
+ this.backendUrl = options.configApi.getString("backend.baseUrl");
15
+ }
16
+ async get(path, headers) {
17
+ return await this.requestRaw(`${this.backendUrl}/${path}`, headers);
18
+ }
19
+ async post(path, headers, data) {
20
+ const hdrs = {
21
+ ...headers,
22
+ "Content-Type": "application/json"
23
+ };
24
+ const method = "POST";
25
+ return await this.requestRaw(
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
+ };
71
+ }
72
+ return await fetch(url, payload);
73
+ }
74
+ async getCostReports(filters, groups, granularity, startTime, endTime) {
75
+ const { token: idToken } = await this.identityApi.getCredentials();
76
+ const headers = idToken ? { Authorization: `Bearer ${idToken}` } : {};
77
+ const url = `api/infrawallet/reports?&filters=${filters}&groups=${groups}&granularity=${granularity}&startTime=${startTime.getTime()}&endTime=${endTime.getTime()}`;
78
+ const response = await this.get(url, headers);
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
+ }
85
+ }
86
+ }
87
+
88
+ export { InfraWalletApiClient };
89
+ //# sourceMappingURL=InfraWalletApiClient.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InfraWalletApiClient.esm.js","sources":["../../src/api/InfraWalletApiClient.ts"],"sourcesContent":["import { ConfigApi, IdentityApi } from '@backstage/core-plugin-api';\nimport fetch, { Response } 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, headers?: Record<string, string>): Promise<Response> {\n return await this.requestRaw(`${this.backendUrl}/${path}`, headers);\n }\n\n async post(\n path: string,\n headers?: Record<string, string>,\n data?: Record<string, any | undefined>,\n ): Promise<Response> {\n const hdrs = {\n ...headers,\n 'Content-Type': 'application/json',\n };\n const method = 'POST';\n\n return await this.requestRaw(\n `${this.backendUrl}/${path}`,\n hdrs,\n method,\n data,\n );\n }\n\n async put(\n path: string,\n headers?: Record<string, string>,\n data?: Record<string, any | undefined>,\n ): Promise<Response> {\n const hdrs = {\n ...headers,\n 'Content-Type': 'application/json',\n };\n const method = 'PUT';\n\n return await this.requestRaw(\n `${this.backendUrl}/${path}`,\n hdrs,\n method,\n data,\n );\n }\n\n async delete(\n path: string,\n headers?: Record<string, string>,\n data?: Record<string, any | undefined>,\n ): Promise<Response> {\n const hdrs = {\n ...headers,\n 'Content-Type': 'application/json',\n };\n const method = 'DELETE';\n\n return await this.requestRaw(\n `${this.backendUrl}/${path}`,\n hdrs,\n method,\n data,\n );\n }\n\n async requestRaw(\n url: string,\n headers?: Record<string, string>,\n method?: string,\n data?: Record<string, any | undefined>,\n ): Promise<Response> {\n let payload;\n if (!method) {\n payload = {\n method: 'GET',\n headers,\n };\n } else {\n payload = {\n method,\n headers,\n body: JSON.stringify(data),\n };\n }\n\n return await fetch(url, payload);\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 { token: idToken } = await this.identityApi.getCredentials();\n const headers = idToken ? { Authorization: `Bearer ${idToken}` } : {};\n\n const url = `api/infrawallet/reports?&filters=${filters}&groups=${groups}&granularity=${granularity}&startTime=${startTime.getTime()}&endTime=${endTime.getTime()}`;\n const response = await this.get(url, headers);\n\n if (!response.ok) {\n const r = await response.json();\n throw new Error(r.error.message);\n } else {\n return await response.json();\n }\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,100 @@
1
+ import { reduce } from 'lodash';
2
+ import { parse, subMonths, format } from 'date-fns';
3
+
4
+ const mergeCostReports = (reports, threshold) => {
5
+ if (reports.length <= threshold) {
6
+ return reports;
7
+ }
8
+ const totalCosts = [];
9
+ reports.forEach((report) => {
10
+ let total = 0;
11
+ Object.values(report.reports).forEach((v) => {
12
+ total += v;
13
+ });
14
+ totalCosts.push({ id: report.id, total });
15
+ });
16
+ const sortedTotalCosts = totalCosts.sort((a, b) => b.total - a.total);
17
+ const idsToBeKept = sortedTotalCosts.slice(0, threshold).map((v) => v.id);
18
+ const mergedReports = reduce(
19
+ reports,
20
+ (acc, report) => {
21
+ let keyName = "others";
22
+ if (idsToBeKept.includes(report.id)) {
23
+ keyName = report.id;
24
+ }
25
+ if (!acc[keyName]) {
26
+ acc[keyName] = {
27
+ id: keyName,
28
+ reports: {}
29
+ };
30
+ }
31
+ Object.keys(report.reports).forEach((key) => {
32
+ if (acc[keyName].reports[key]) {
33
+ acc[keyName].reports[key] += report.reports[key];
34
+ } else {
35
+ acc[keyName].reports[key] = report.reports[key];
36
+ }
37
+ });
38
+ return acc;
39
+ },
40
+ {}
41
+ );
42
+ return Object.values(mergedReports);
43
+ };
44
+ const aggregateCostReports = (reports, aggregatedBy) => {
45
+ const aggregatedReports = reduce(
46
+ reports,
47
+ (acc, report) => {
48
+ let keyName = "no value";
49
+ if (aggregatedBy && aggregatedBy in report) {
50
+ keyName = report[aggregatedBy];
51
+ } else if (aggregatedBy === "none") {
52
+ keyName = "Total cloud costs";
53
+ }
54
+ if (!acc[keyName]) {
55
+ acc[keyName] = {
56
+ id: keyName,
57
+ reports: {}
58
+ };
59
+ acc[keyName][aggregatedBy] = keyName;
60
+ }
61
+ Object.keys(report.reports).forEach((key) => {
62
+ if (acc[keyName].reports[key]) {
63
+ acc[keyName].reports[key] += report.reports[key];
64
+ } else {
65
+ acc[keyName].reports[key] = report.reports[key];
66
+ }
67
+ });
68
+ return acc;
69
+ },
70
+ {}
71
+ );
72
+ return Object.values(aggregatedReports);
73
+ };
74
+ const getAllReportTags = (reports) => {
75
+ const tags = /* @__PURE__ */ new Set();
76
+ const reservedKeys = [
77
+ "id",
78
+ "name",
79
+ "service",
80
+ "category",
81
+ "provider",
82
+ "reports"
83
+ ];
84
+ reports.forEach((report) => {
85
+ Object.keys(report).forEach((key) => {
86
+ if (reservedKeys.indexOf(key) === -1) {
87
+ tags.add(key);
88
+ }
89
+ });
90
+ });
91
+ return Array.from(tags);
92
+ };
93
+ const getPreviousMonth = (month) => {
94
+ const date = parse(month, "yyyy-MM", /* @__PURE__ */ new Date());
95
+ const previousMonth = subMonths(date, 1);
96
+ return format(previousMonth, "yyyy-MM");
97
+ };
98
+
99
+ export { aggregateCostReports, getAllReportTags, getPreviousMonth, mergeCostReports };
100
+ //# sourceMappingURL=functions.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"functions.esm.js","sources":["../../src/api/functions.ts"],"sourcesContent":["import { Report } from './types';\nimport { reduce } from 'lodash';\nimport { parse, format, subMonths } from 'date-fns';\n\nexport const mergeCostReports = (\n reports: Report[],\n threshold: number,\n): Report[] => {\n if (reports.length <= threshold) {\n return reports;\n }\n const totalCosts = [];\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 (acc, report) => {\n let keyName = 'others';\n if (idsToBeKept.includes(report.id)) {\n keyName = report.id;\n }\n if (!acc[keyName]) {\n acc[keyName] = {\n id: keyName,\n reports: {},\n };\n }\n\n Object.keys(report.reports).forEach(key => {\n if (acc[keyName].reports[key]) {\n acc[keyName].reports[key] += report.reports[key];\n } else {\n acc[keyName].reports[key] = report.reports[key];\n }\n });\n return acc;\n },\n {},\n );\n\n return Object.values(mergedReports);\n};\n\nexport const aggregateCostReports = (\n reports: Report[],\n aggregatedBy?: string,\n): Report[] => {\n const aggregatedReports = reduce(\n reports,\n (acc, report) => {\n let keyName = 'no value';\n if (aggregatedBy && aggregatedBy in report) {\n keyName = report[aggregatedBy];\n } else if (aggregatedBy === 'none') {\n keyName = 'Total cloud costs';\n }\n if (!acc[keyName]) {\n acc[keyName] = {\n id: keyName,\n reports: {},\n };\n acc[keyName][aggregatedBy] = keyName;\n }\n\n Object.keys(report.reports).forEach(key => {\n if (acc[keyName].reports[key]) {\n acc[keyName].reports[key] += report.reports[key];\n } else {\n acc[keyName].reports[key] = report.reports[key];\n }\n });\n return acc;\n },\n {},\n );\n return Object.values(aggregatedReports);\n};\n\nexport const getAllReportTags = (reports: Report[]): string[] => {\n const tags = new Set<string>();\n const reservedKeys = [\n 'id',\n 'name',\n 'service',\n 'category',\n 'provider',\n 'reports',\n ];\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"],"names":[],"mappings":"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"}
@@ -0,0 +1,168 @@
1
+ import { Paper } from '@material-ui/core';
2
+ import { useTheme, makeStyles } from '@material-ui/core/styles';
3
+ import humanFormat from 'human-format';
4
+ import React from 'react';
5
+ import Chart from 'react-apexcharts';
6
+
7
+ const ColumnsChartComponent = ({
8
+ categories,
9
+ series,
10
+ height,
11
+ thumbnail,
12
+ dataPointSelectionHandler
13
+ }) => {
14
+ const defaultTheme = useTheme();
15
+ const useStyles = makeStyles({
16
+ fixedHeightPaper: {
17
+ padding: "16px",
18
+ display: "flex",
19
+ overflow: "auto",
20
+ flexDirection: "column",
21
+ height: height ? height : 300
22
+ },
23
+ thumbnailPaper: {
24
+ display: "flex",
25
+ overflow: "auto",
26
+ flexDirection: "column",
27
+ height: height ? height - 50 : 100
28
+ }
29
+ });
30
+ const classes = useStyles();
31
+ const customScale = humanFormat.Scale.create(["", "K", "M", "B"], 1e3);
32
+ const state = thumbnail ? {
33
+ options: {
34
+ chart: {
35
+ animations: {
36
+ enabled: false
37
+ },
38
+ zoom: {
39
+ enabled: false
40
+ },
41
+ stacked: true,
42
+ toolbar: {
43
+ show: false
44
+ },
45
+ sparkline: {
46
+ enabled: true
47
+ }
48
+ },
49
+ xaxis: {
50
+ categories
51
+ },
52
+ theme: {
53
+ mode: defaultTheme.palette.type
54
+ }
55
+ },
56
+ series
57
+ } : {
58
+ options: {
59
+ chart: {
60
+ animations: {
61
+ enabled: false
62
+ },
63
+ stacked: true,
64
+ toolbar: {
65
+ show: true
66
+ },
67
+ events: {
68
+ dataPointSelection: dataPointSelectionHandler
69
+ }
70
+ },
71
+ xaxis: {
72
+ categories
73
+ },
74
+ yaxis: {
75
+ decimalsInFloat: 2
76
+ },
77
+ dataLabels: {
78
+ formatter: (val) => {
79
+ if (val) {
80
+ return `$${humanFormat(val, {
81
+ scale: customScale,
82
+ separator: ""
83
+ })}`;
84
+ }
85
+ return "null";
86
+ }
87
+ },
88
+ legend: {
89
+ showForSingleSeries: true
90
+ },
91
+ theme: {
92
+ mode: defaultTheme.palette.type
93
+ },
94
+ // there are only 5 colors by default, here we extend it to 50 different colors
95
+ colors: [
96
+ "#008FFB",
97
+ "#00E396",
98
+ "#FEB019",
99
+ "#FF4560",
100
+ "#775DD0",
101
+ "#3F51B5",
102
+ "#03A9F4",
103
+ "#4CAF50",
104
+ "#F9CE1D",
105
+ "#FF9800",
106
+ "#33B2DF",
107
+ "#546E7A",
108
+ "#D4526E",
109
+ "#13D8AA",
110
+ "#A5978B",
111
+ "#4ECDC4",
112
+ "#C7F464",
113
+ "#81D4FA",
114
+ "#546E7A",
115
+ "#FD6A6A",
116
+ "#2B908F",
117
+ "#F9A3A4",
118
+ "#90EE7E",
119
+ "#FA4443",
120
+ "#69D2E7",
121
+ "#449DD1",
122
+ "#F86624",
123
+ "#EA3546",
124
+ "#662E9B",
125
+ "#C5D86D",
126
+ "#D7263D",
127
+ "#1B998B",
128
+ "#2E294E",
129
+ "#F46036",
130
+ "#E2C044",
131
+ "#662E9B",
132
+ "#F86624",
133
+ "#F9C80E",
134
+ "#EA3546",
135
+ "#43BCCD",
136
+ "#5C4742",
137
+ "#A5978B",
138
+ "#8D5B4C",
139
+ "#5A2A27",
140
+ "#C4BBAF",
141
+ "#A300D6",
142
+ "#7D02EB",
143
+ "#5653FE",
144
+ "#2983FF",
145
+ "#00B1F2"
146
+ ]
147
+ },
148
+ series
149
+ };
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
+ );
165
+ };
166
+
167
+ export { ColumnsChartComponent };
168
+ //# sourceMappingURL=ColumnsChartComponent.esm.js.map
@@ -0,0 +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: true,\n },\n events: {\n dataPointSelection: dataPointSelectionHandler,\n },\n },\n xaxis: {\n categories: categories,\n },\n yaxis: {\n decimalsInFloat: 2,\n },\n dataLabels: {\n formatter: (val: number) => {\n if (val) {\n return `$${humanFormat(val, {\n scale: customScale,\n separator: '',\n })}`;\n }\n return 'null';\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\n className={thumbnail ? classes.thumbnailPaper : classes.fixedHeightPaper}\n >\n <Chart\n options={state.options}\n series={state.series}\n type=\"bar\"\n height={height ? height - 50 : 250}\n />\n </Paper>\n );\n};\n"],"names":[],"mappings":"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"}