Skip to content

Commit b750dbe

Browse files
authored
FLPATH-2731 | [FE] Currency display and settings & FLPATH-2737 | [BE] Backend plugin to proxy calls (#1748)
* [FE] Main view (adjustable table + pagination + sorting) * Create Shared files for common code
1 parent 7f9c6c9 commit b750dbe

19 files changed

Lines changed: 1490 additions & 29 deletions

File tree

workspaces/redhat-resource-optimization/packages/app/src/components/Root/Root.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ import {
3636
SidebarPage,
3737
SidebarSpace,
3838
useSidebarOpenState,
39+
SidebarSubmenuItem,
3940
Link,
41+
SidebarSubmenu,
4042
} from '@backstage/core-components';
4143
import MenuIcon from '@material-ui/icons/Menu';
4244
import SearchIcon from '@material-ui/icons/Search';

workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-common/src/clients/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@
1616

1717
export * from './optimizations';
1818
export * from './orchestrator-slim';
19+
export * from './types/cost-management';

workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-common/src/clients/optimizations/OptimizationsClient.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,22 @@ import type {
3737
OptimizationsApi,
3838
GetAccessResponse,
3939
} from './types';
40+
import type {
41+
GetCostManagementRequest,
42+
CostManagementReport,
43+
} from '../types/cost-management';
4044
import { UnauthorizedError } from '@backstage-community/plugin-rbac-common';
4145
import { AuthorizeResult } from '@backstage/plugin-permission-common';
4246

4347
type DefaultApiClientOpFunc<
44-
TRequest = GetRecommendationByIdRequest | GetRecommendationListRequest,
45-
TResponse = RecommendationBoxPlots | RecommendationList,
48+
TRequest =
49+
| GetRecommendationByIdRequest
50+
| GetRecommendationListRequest
51+
| GetCostManagementRequest,
52+
TResponse =
53+
| RecommendationBoxPlots
54+
| RecommendationList
55+
| CostManagementReport,
4656
> = (
4757
this: DefaultApiClient,
4858
request: TRequest,
@@ -142,6 +152,44 @@ export class OptimizationsClient implements OptimizationsApi {
142152
};
143153
}
144154

155+
public async getCostManagementReport(
156+
request: GetCostManagementRequest,
157+
): Promise<TypedResponse<CostManagementReport>> {
158+
// Get access permission
159+
const accessAPIResponse = await this.getAccess();
160+
161+
if (accessAPIResponse.decision === AuthorizeResult.DENY) {
162+
throw new UnauthorizedError();
163+
}
164+
165+
// Get or refresh token
166+
if (!this.token) {
167+
const { accessToken } = await this.getNewToken();
168+
this.token = accessToken;
169+
}
170+
171+
// Call the cost-management API via backend proxy
172+
let response = await this.defaultClient.getCostManagementReport(request, {
173+
token: this.token,
174+
});
175+
176+
// Handle 401 errors by refreshing token and retrying
177+
if (!response.ok && response.status === 401) {
178+
const { accessToken } = await this.getNewToken();
179+
this.token = accessToken;
180+
181+
response = await this.defaultClient.getCostManagementReport(request, {
182+
token: this.token,
183+
});
184+
}
185+
186+
if (!response.ok) {
187+
throw new Error(response.statusText);
188+
}
189+
190+
return response;
191+
}
192+
145193
private async getAccess(): Promise<GetAccessResponse> {
146194
const baseUrl = await this.discoveryApi.getBaseUrl(`${pluginId}`);
147195
const response = await this.fetchApi.fetch(`${baseUrl}/access`);

workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-common/src/clients/optimizations/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ export type OptimizationsApi = Omit<
2222
'fetchApi' | 'discoveryApi'
2323
>;
2424

25+
/** @public */
26+
export type GetCostManagementRequest = Parameters<
27+
OptimizationsApi['getCostManagementReport']
28+
>[0];
29+
2530
/**
2631
* This is a copy of GetTokenResponse, to avoid importing redhat-resource-optimization-backend.
2732
*
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/** @public */
18+
export interface CostValue {
19+
value: number;
20+
units: string;
21+
}
22+
23+
/** @public */
24+
export interface BasicCost {
25+
raw: CostValue;
26+
markup: CostValue;
27+
usage: CostValue;
28+
total: CostValue;
29+
}
30+
31+
/** @public */
32+
export interface DistributedCost extends BasicCost {
33+
platform_distributed: CostValue;
34+
worker_unallocated_distributed: CostValue;
35+
network_unattributed_distributed: CostValue;
36+
storage_unattributed_distributed: CostValue;
37+
distributed: CostValue;
38+
}
39+
40+
/** @public */
41+
export interface ProjectValue {
42+
date: string;
43+
project: string;
44+
cost_group: number | string;
45+
classification: string;
46+
source_uuid: string[];
47+
clusters: string[];
48+
delta_value: number;
49+
delta_percent: number;
50+
infrastructure: BasicCost;
51+
supplementary: BasicCost;
52+
cost: DistributedCost;
53+
}
54+
55+
/** @public */
56+
export interface Project {
57+
project: string;
58+
values: ProjectValue[];
59+
}
60+
61+
/** @public */
62+
export interface DateData {
63+
date: string;
64+
projects: Project[];
65+
}
66+
67+
/** @public */
68+
export interface CostManagementReport {
69+
meta: {
70+
count: number;
71+
limit: number;
72+
offset: number;
73+
others: number;
74+
currency: string;
75+
delta: {
76+
value: number;
77+
percent: number;
78+
};
79+
filter: {
80+
resolution: string;
81+
time_scope_value: string;
82+
time_scope_units: string;
83+
limit: number;
84+
offset: number;
85+
};
86+
group_by: {
87+
[key: string]: string[];
88+
};
89+
order_by: {
90+
[key: string]: string;
91+
};
92+
exclude: Record<string, unknown>;
93+
distributed_overhead: boolean;
94+
total: {
95+
infrastructure: BasicCost;
96+
supplementary: BasicCost;
97+
cost: DistributedCost;
98+
};
99+
};
100+
links: {
101+
first: string;
102+
next: string | null;
103+
previous: string | null;
104+
last: string;
105+
};
106+
data: DateData[];
107+
}
108+
109+
/** @public */
110+
export interface GetCostManagementRequest {
111+
query: {
112+
currency?: 'USD' | 'EUR' | 'GBP';
113+
delta?: string;
114+
'filter[limit]'?: number;
115+
'filter[offset]'?: number;
116+
'filter[resolution]'?: 'daily' | 'monthly';
117+
'filter[time_scope_units]'?: 'day' | 'month';
118+
'filter[time_scope_value]'?: number;
119+
'group_by[project]'?: '*' | string;
120+
'order_by[distributed_cost]'?: 'asc' | 'desc';
121+
'order_by[markup_cost]'?: 'asc' | 'desc';
122+
'order_by[raw_cost]'?: 'asc' | 'desc';
123+
};
124+
}

workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-common/src/generated/apis/DefaultApi.client.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import * as parser from 'uri-template';
2525

2626
import { RecommendationBoxPlots } from '../models/RecommendationBoxPlots.model';
2727
import { RecommendationList } from '../models/RecommendationList.model';
28+
import type { CostManagementReport } from '../../clients/types/cost-management';
2829

2930
/**
3031
* Wraps the Response type to convey a type on the json call.
@@ -154,4 +155,109 @@ export class DefaultApiClient {
154155
method: 'GET',
155156
});
156157
}
158+
159+
/**
160+
* Get cost management report for OpenShift projects
161+
* @param currency - Currency preference (USD, EUR, GBP)
162+
* @param delta - Delta calculation method
163+
* @param filter - Filter parameters
164+
* @param group_by - Group by parameters
165+
* @param order_by - Order by parameters
166+
*/
167+
public async getCostManagementReport(
168+
request: {
169+
query: {
170+
currency?: 'USD' | 'EUR' | 'GBP';
171+
delta?: string;
172+
'filter[limit]'?: number;
173+
'filter[offset]'?: number;
174+
'filter[resolution]'?: 'daily' | 'monthly';
175+
'filter[time_scope_units]'?: 'day' | 'month';
176+
'filter[time_scope_value]'?: number;
177+
'group_by[project]'?: '*' | string;
178+
'order_by[distributed_cost]'?: 'asc' | 'desc';
179+
'order_by[markup_cost]'?: 'asc' | 'desc';
180+
'order_by[raw_cost]'?: 'asc' | 'desc';
181+
};
182+
},
183+
options?: RequestOptions,
184+
): Promise<TypedResponse<CostManagementReport>> {
185+
// Get the proxy base URL for cost-management API
186+
const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
187+
const uri = '/reports/openshift/costs/';
188+
189+
// Build query string manually
190+
const queryParams = new URLSearchParams();
191+
if (request.query.currency) {
192+
queryParams.append('currency', request.query.currency);
193+
}
194+
if (request.query.delta) {
195+
queryParams.append('delta', request.query.delta);
196+
}
197+
if (request.query['filter[limit]']) {
198+
queryParams.append(
199+
'filter[limit]',
200+
String(request.query['filter[limit]']),
201+
);
202+
}
203+
if (request.query['filter[offset]']) {
204+
queryParams.append(
205+
'filter[offset]',
206+
String(request.query['filter[offset]']),
207+
);
208+
}
209+
if (request.query['filter[resolution]']) {
210+
queryParams.append(
211+
'filter[resolution]',
212+
request.query['filter[resolution]'],
213+
);
214+
}
215+
if (request.query['filter[time_scope_units]']) {
216+
queryParams.append(
217+
'filter[time_scope_units]',
218+
request.query['filter[time_scope_units]'],
219+
);
220+
}
221+
if (request.query['filter[time_scope_value]']) {
222+
queryParams.append(
223+
'filter[time_scope_value]',
224+
String(request.query['filter[time_scope_value]']),
225+
);
226+
}
227+
if (request.query['group_by[project]']) {
228+
queryParams.append(
229+
'group_by[project]',
230+
request.query['group_by[project]'],
231+
);
232+
}
233+
if (request.query['order_by[distributed_cost]']) {
234+
queryParams.append(
235+
'order_by[distributed_cost]',
236+
request.query['order_by[distributed_cost]'],
237+
);
238+
}
239+
if (request.query['order_by[markup_cost]']) {
240+
queryParams.append(
241+
'order_by[markup_cost]',
242+
request.query['order_by[markup_cost]'],
243+
);
244+
}
245+
if (request.query['order_by[raw_cost]']) {
246+
queryParams.append(
247+
'order_by[raw_cost]',
248+
request.query['order_by[raw_cost]'],
249+
);
250+
}
251+
252+
const queryString = queryParams.toString();
253+
const url = `${baseUrl}${uri}${queryString ? `?${queryString}` : ''}`;
254+
255+
return await this.fetchApi.fetch(url, {
256+
headers: {
257+
'Content-Type': 'application/json',
258+
...(options?.token && { Authorization: `Bearer ${options?.token}` }),
259+
},
260+
method: 'GET',
261+
});
262+
}
157263
}

workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/pages/optimizations/components/PageLayout.tsx renamed to workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/components/PageLayout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ function Table(props: { children: React.ReactNode }) {
8181
);
8282
}
8383

84+
/** @public */
8485
export function PageLayout(props: { children: React.ReactNode }) {
8586
return (
8687
<Grid container style={{ position: 'relative' }}>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { makeStyles, Theme } from '@material-ui/core/styles';
18+
19+
/**
20+
* Common base styles for Filters components.
21+
* These styles are shared between OpenShift and Optimizations filters.
22+
*
23+
* @public
24+
*/
25+
export const useBaseFiltersStyles = makeStyles(
26+
(theme: Theme) => ({
27+
root: {
28+
height: '100%',
29+
width: '100%',
30+
display: 'flex',
31+
flexDirection: 'column',
32+
marginRight: theme.spacing(3),
33+
},
34+
value: {
35+
fontWeight: 'bold',
36+
fontSize: 18,
37+
},
38+
header: {
39+
display: 'flex',
40+
alignItems: 'center',
41+
height: theme.spacing(7.5),
42+
justifyContent: 'space-between',
43+
borderBottom: `1px solid ${theme.palette.grey[500]}`,
44+
},
45+
filters: {
46+
display: 'flex',
47+
flexDirection: 'column',
48+
},
49+
}),
50+
{ name: 'BaseFilters' },
51+
);

0 commit comments

Comments
 (0)