Skip to content

Commit 861098c

Browse files
authored
Enable group_by and filter_by tag. (#1838)
1 parent 4e14f2f commit 861098c

8 files changed

Lines changed: 389 additions & 34 deletions

File tree

workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-backend/src/service/router.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ describe('createRouter', () => {
3636
searchOpenShiftProjects: jest.fn(),
3737
searchOpenShiftClusters: jest.fn(),
3838
searchOpenShiftNodes: jest.fn(),
39+
getOpenShiftTags: jest.fn(),
40+
getOpenShiftTagValues: jest.fn(),
3941
},
4042
});
4143
app = express().use(router);

workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-common/report-clients.api.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,27 @@ export type OptimizationsApi = Omit<
241241
links?: any;
242242
}>
243243
>;
244+
getOpenShiftTags(timeScopeValue?: number): Promise<
245+
TypedResponse<{
246+
data: string[];
247+
meta?: any;
248+
links?: any;
249+
}>
250+
>;
251+
getOpenShiftTagValues(
252+
tagKey: string,
253+
timeScopeValue?: number,
254+
): Promise<
255+
TypedResponse<{
256+
data: Array<{
257+
key: string;
258+
values: string[];
259+
enabled: boolean;
260+
}>;
261+
meta?: any;
262+
links?: any;
263+
}>
264+
>;
244265
};
245266

246267
// @public
@@ -250,6 +271,27 @@ export class OptimizationsClient implements OptimizationsApi {
250271
getCostManagementReport(
251272
request: GetCostManagementRequest,
252273
): Promise<TypedResponse<CostManagementReport>>;
274+
getOpenShiftTags(timeScopeValue?: number): Promise<
275+
TypedResponse<{
276+
data: string[];
277+
meta?: any;
278+
links?: any;
279+
}>
280+
>;
281+
getOpenShiftTagValues(
282+
tagKey: string,
283+
timeScopeValue?: number,
284+
): Promise<
285+
TypedResponse<{
286+
data: Array<{
287+
key: string;
288+
values: string[];
289+
enabled: boolean;
290+
}>;
291+
meta?: any;
292+
links?: any;
293+
}>
294+
>;
253295
// Warning: (ae-forgotten-export) The symbol "RecommendationBoxPlots" needs to be exported by the entry point index.d.ts
254296
//
255297
// (undocumented)

workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-common/report.api.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,27 @@ export type OptimizationsApi = Omit<
571571
links?: any;
572572
}>
573573
>;
574+
getOpenShiftTags(timeScopeValue?: number): Promise<
575+
TypedResponse<{
576+
data: string[];
577+
meta?: any;
578+
links?: any;
579+
}>
580+
>;
581+
getOpenShiftTagValues(
582+
tagKey: string,
583+
timeScopeValue?: number,
584+
): Promise<
585+
TypedResponse<{
586+
data: Array<{
587+
key: string;
588+
values: string[];
589+
enabled: boolean;
590+
}>;
591+
meta?: any;
592+
links?: any;
593+
}>
594+
>;
574595
};
575596

576597
// @public
@@ -580,6 +601,27 @@ export class OptimizationsClient implements OptimizationsApi {
580601
getCostManagementReport(
581602
request: GetCostManagementRequest,
582603
): Promise<TypedResponse<CostManagementReport>>;
604+
getOpenShiftTags(timeScopeValue?: number): Promise<
605+
TypedResponse<{
606+
data: string[];
607+
meta?: any;
608+
links?: any;
609+
}>
610+
>;
611+
getOpenShiftTagValues(
612+
tagKey: string,
613+
timeScopeValue?: number,
614+
): Promise<
615+
TypedResponse<{
616+
data: Array<{
617+
key: string;
618+
values: string[];
619+
enabled: boolean;
620+
}>;
621+
meta?: any;
622+
links?: any;
623+
}>
624+
>;
583625
// (undocumented)
584626
getRecommendationById(
585627
request: GetRecommendationByIdRequest,

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

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,78 @@ export class OptimizationsClient implements OptimizationsApi {
336336
return await this.fetchResourceType(url);
337337
}
338338

339+
/**
340+
* Get OpenShift tags
341+
* @param timeScopeValue - Time scope value (-1 for month-to-date, -2 for previous-month). Defaults to -1.
342+
* @returns TypedResponse with array of tag strings
343+
*/
344+
public async getOpenShiftTags(
345+
timeScopeValue: number = -1,
346+
): Promise<TypedResponse<{ data: string[]; meta?: any; links?: any }>> {
347+
const baseUrl = await this.discoveryApi.getBaseUrl('proxy');
348+
const url = `${baseUrl}/cost-management/v1/tags/openshift/?filter[time_scope_value]=${timeScopeValue}&key_only=true&limit=1000`;
349+
350+
// Get access permission
351+
const accessAPIResponse = await this.getAccess();
352+
353+
if (accessAPIResponse.decision === AuthorizeResult.DENY) {
354+
throw new UnauthorizedError();
355+
}
356+
357+
// Get or refresh token
358+
if (!this.token) {
359+
const { accessToken } = await this.getNewToken();
360+
this.token = accessToken;
361+
}
362+
363+
return await this.fetchWithTokenAndRetry<{
364+
data: string[];
365+
meta?: any;
366+
links?: any;
367+
}>(url);
368+
}
369+
370+
/**
371+
* Get OpenShift tag values for a specific tag key
372+
* @param tagKey - The tag key to get values for
373+
* @param timeScopeValue - Time scope value (-1 for month-to-date, -2 for previous-month). Defaults to -1.
374+
* @returns TypedResponse with array of tag value objects
375+
*/
376+
public async getOpenShiftTagValues(
377+
tagKey: string,
378+
timeScopeValue: number = -1,
379+
): Promise<
380+
TypedResponse<{
381+
data: Array<{ key: string; values: string[]; enabled: boolean }>;
382+
meta?: any;
383+
links?: any;
384+
}>
385+
> {
386+
const baseUrl = await this.discoveryApi.getBaseUrl('proxy');
387+
const url = `${baseUrl}/cost-management/v1/tags/openshift/?filter[key]=${encodeURIComponent(
388+
tagKey,
389+
)}&filter[time_scope_value]=${timeScopeValue}`;
390+
391+
// Get access permission
392+
const accessAPIResponse = await this.getAccess();
393+
394+
if (accessAPIResponse.decision === AuthorizeResult.DENY) {
395+
throw new UnauthorizedError();
396+
}
397+
398+
// Get or refresh token
399+
if (!this.token) {
400+
const { accessToken } = await this.getNewToken();
401+
this.token = accessToken;
402+
}
403+
404+
return await this.fetchWithTokenAndRetry<{
405+
data: Array<{ key: string; values: string[]; enabled: boolean }>;
406+
meta?: any;
407+
links?: any;
408+
}>(url);
409+
}
410+
339411
private async fetchResourceType(
340412
url: string,
341413
): Promise<

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ export type OptimizationsApi = Omit<
6666
): Promise<
6767
TypedResponse<{ data: Array<{ value: string }>; meta?: any; links?: any }>
6868
>;
69+
getOpenShiftTags(
70+
timeScopeValue?: number,
71+
): Promise<TypedResponse<{ data: string[]; meta?: any; links?: any }>>;
72+
getOpenShiftTagValues(
73+
tagKey: string,
74+
timeScopeValue?: number,
75+
): Promise<
76+
TypedResponse<{
77+
data: Array<{ key: string; values: string[]; enabled: boolean }>;
78+
meta?: any;
79+
links?: any;
80+
}>
81+
>;
6982
};
7083

7184
/**

workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/pages/openshift/OpenShiftPage.tsx

Lines changed: 87 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,41 @@ export function OpenShiftPage() {
7070
const [pageSize, setPageSize] = useState(5);
7171
const [sortField, setSortField] = useState<string | null>(null);
7272
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
73+
const [tags, setTags] = useState<string[]>([]);
74+
const [selectedTag, setSelectedTag] = useState<string>('');
75+
const [selectedTagKey, setSelectedTagKey] = useState<string>('');
76+
const [selectedTagValue, setSelectedTagValue] = useState<string>('');
77+
78+
// Fetch tags on first load
79+
useAsync(async () => {
80+
try {
81+
const timeScopeValue = timeRange === 'month-to-date' ? -1 : -2;
82+
const response = await api.getOpenShiftTags(timeScopeValue);
83+
const tagsData = await response.json();
84+
setTags(tagsData.data || []);
85+
} catch {
86+
// Silently fail if tags can't be loaded
87+
setTags([]);
88+
}
89+
}, [api, timeRange]);
7390

7491
const {
7592
value: costData,
7693
loading,
7794
error,
7895
} = useAsync(async () => {
96+
// Don't make API call if groupBy is 'tag' but no tag is selected
97+
if (groupBy === 'tag' && !selectedTag) {
98+
return null;
99+
}
100+
79101
try {
80-
const groupByParam = `group_by[${groupBy}]`;
102+
let groupByParam: string;
103+
if (groupBy === 'tag') {
104+
groupByParam = `group_by[tag:${selectedTag}]`;
105+
} else {
106+
groupByParam = `group_by[${groupBy}]`;
107+
}
81108

82109
let deltaParam = 'cost';
83110
if (groupBy === 'project' && overheadDistribution === 'distribute') {
@@ -100,7 +127,10 @@ export function OpenShiftPage() {
100127

101128
queryParams[groupByParam] = '*';
102129

103-
if (filterValue) {
130+
// Handle tag filtering differently
131+
if (filterBy === 'tag' && selectedTagKey && selectedTagValue) {
132+
queryParams[`filter[tag:${selectedTagKey}]`] = selectedTagValue;
133+
} else if (filterValue) {
104134
queryParams[`filter[${filterBy}]`] = filterValue;
105135
}
106136

@@ -139,10 +169,39 @@ export function OpenShiftPage() {
139169
pageSize,
140170
sortField,
141171
sortDirection,
172+
selectedTag,
173+
filterBy,
174+
selectedTagKey,
175+
selectedTagValue,
142176
]);
143177

144178
const displayData = useMemo(() => {
145-
if (!costData) return null;
179+
// If costData is null, return empty structure to keep table visible during loading
180+
if (!costData) {
181+
const today = new Date();
182+
const month =
183+
timeRange === 'previous-month'
184+
? new Date(
185+
today.getFullYear(),
186+
today.getMonth() - 1,
187+
1,
188+
).toLocaleString('en-US', { month: 'long' })
189+
: today.toLocaleString('en-US', { month: 'long' });
190+
const endDate =
191+
timeRange === 'previous-month'
192+
? new Date(today.getFullYear(), today.getMonth(), 0)
193+
.getDate()
194+
.toString()
195+
: today.getDate().toString();
196+
197+
return {
198+
totalCost: 0,
199+
month,
200+
endDate,
201+
currencyCode: currency,
202+
projects: [],
203+
};
204+
}
146205

147206
const arrayKey = `${groupBy}s` as keyof (typeof costData.data)[0];
148207
const groupedArray = costData.data?.[0]?.[arrayKey] as
@@ -437,14 +496,6 @@ export function OpenShiftPage() {
437496
return <ResponseErrorPanel error={error} />;
438497
}
439498

440-
if (!displayData) {
441-
return (
442-
<BasePage pageTitle="" withContentPadding>
443-
<div>Loading...</div>
444-
</BasePage>
445-
);
446-
}
447-
448499
return (
449500
<BasePage pageTitle="" withContentPadding>
450501
<PageHeader
@@ -474,9 +525,18 @@ export function OpenShiftPage() {
474525
filterBy={filterBy}
475526
filterOperation={filterOperation}
476527
filterValue={filterValue}
528+
tags={tags}
477529
onGroupByChange={value => {
478530
setGroupBy(value);
479531
setCurrentPage(0);
532+
if (value !== 'tag') {
533+
setSelectedTag('');
534+
}
535+
}}
536+
selectedTag={selectedTag}
537+
onSelectedTagChange={value => {
538+
setSelectedTag(value);
539+
setCurrentPage(0);
480540
}}
481541
onOverheadDistributionChange={value => {
482542
setOverheadDistribution(value);
@@ -493,6 +553,11 @@ export function OpenShiftPage() {
493553
onFilterByChange={value => {
494554
setFilterBy(value);
495555
setCurrentPage(0);
556+
// Clear tag-related selections when filterBy changes away from 'tag'
557+
if (value !== 'tag') {
558+
setSelectedTagKey('');
559+
setSelectedTagValue('');
560+
}
496561
}}
497562
onFilterOperationChange={value => {
498563
setFilterOperation(value);
@@ -502,6 +567,17 @@ export function OpenShiftPage() {
502567
setFilterValue(value);
503568
setCurrentPage(0);
504569
}}
570+
selectedTagKey={selectedTagKey}
571+
selectedTagValue={selectedTagValue}
572+
onSelectedTagKeyChange={value => {
573+
setSelectedTagKey(value);
574+
setSelectedTagValue('');
575+
setCurrentPage(0);
576+
}}
577+
onSelectedTagValueChange={value => {
578+
setSelectedTagValue(value);
579+
setCurrentPage(0);
580+
}}
505581
/>
506582
</PageLayout.Filters>
507583
<PageLayout.Table>

workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/pages/openshift/components/AutocompleteComponent.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,12 @@ export function AutocompleteComponent(props: AutocompleteComponentProps) {
8080

8181
return (
8282
<Box className={className ?? classes.root} pb={1} pt={1}>
83-
<Typography className={classes.label} variant="button" component="label">
83+
<Typography
84+
className={classes.label}
85+
variant="button"
86+
component="label"
87+
style={{ textTransform: 'none' }}
88+
>
8489
{label}
8590
</Typography>
8691
<Autocomplete

0 commit comments

Comments
 (0)