Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure tag key values are available for given date range #4149

Merged
merged 1 commit into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions src/routes/components/dataToolbar/dataToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ interface DataToolbarOwnProps {
className?: string;
dateRange?: React.ReactNode; // Optional date range controls to display in toolbar
datePicker?: React.ReactNode; // Optional date picker controls to display in toolbar
endDate?: string; // For Cost Explorer tag key value
groupBy?: string; // Sync category selection with groupBy value
isAllSelected?: boolean;
isBulkSelectDisabled?: boolean;
Expand All @@ -72,9 +73,11 @@ interface DataToolbarOwnProps {
showExport?: boolean; // Show export icon
showFilter?: boolean; // Show export icon
showPlatformCosts?: boolean; // Show platform costs switch
startDate?: string; // For Cost Explorer tag key value
style?: React.CSSProperties;
tagPathsType?: TagPathsType;
tagReport?: Tag; // Data containing tag key and value data
timeScopeValue?: number;
}

interface DataToolbarState {
Expand Down Expand Up @@ -572,21 +575,24 @@ export class DataToolbarBase extends React.Component<DataToolbarProps, DataToolb
// Tag value select

public getTagValueSelect = (tagKeyOption: ToolbarChipGroupExt) => {
const { isDisabled, tagPathsType } = this.props;
const { endDate, isDisabled, startDate, tagPathsType, timeScopeValue } = this.props;
const { currentCategory, currentTagKey, filters, tagKeyValueInput } = this.state;

return getTagValueSelect({
currentCategory,
currentTagKey,
endDate,
filters,
isDisabled,
onDelete: this.handleOnDelete,
onTagValueSelect: this.handleOnTagValueSelect,
onTagValueInput: this.handleOnTagValueInput,
onTagValueInputChange: this.handleOnTagValueInputChange,
startDate,
tagKeyValueInput,
tagKeyOption,
tagPathsType,
timeScopeValue,
});
};

Expand Down Expand Up @@ -741,9 +747,9 @@ export class DataToolbarBase extends React.Component<DataToolbarProps, DataToolb
showFilter,
showPlatformCosts,
style,
query,
tagReport,
} = this.props;
const { filters } = this.state;

const options = categoryOptions ? categoryOptions : getDefaultCategoryOptions();
const filteredOptions = options.filter(
Expand All @@ -770,7 +776,7 @@ export class DataToolbarBase extends React.Component<DataToolbarProps, DataToolb
this.getCostCategoryValueSelectComponent(option)
)}
{this.getTagKeySelectComponent()}
{getTagKeyOptions(tagReport, query).map(option => this.getTagValueSelect(option))}
{getTagKeyOptions(tagReport, filters).map(option => this.getTagValueSelect(option))}
{this.getOrgUnitSelectComponent()}
{filteredOptions.map(option => this.getCategoryInputComponent(option))}
{filteredOptions.map(option => this.getCustomSelectComponent(option))}
Expand Down
50 changes: 44 additions & 6 deletions src/routes/components/dataToolbar/tagValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@ import type { RouterComponentProps } from 'utils/router';
import { withRouter } from 'utils/router';

interface TagValueOwnProps extends RouterComponentProps, WrappedComponentProps {
endDate?: string;
isDisabled?: boolean;
onTagValueSelect(event, selection);
onTagValueInput(event);
onTagValueInputChange(value: string);
selections?: SelectWrapperOption[];
startDate?: string;
tagKey: string;
tagKeyValue: string;
tagPathsType: TagPathsType;
timeScopeValue?: number;
}

interface TagValueStateProps {
Expand Down Expand Up @@ -71,6 +74,29 @@ class TagValueBase extends React.Component<TagValueProps, TagValueState> {
}
}

// Ensure tag key values are available for given date range
private getSelections() {
const { selections, tagKey, tagReport } = this.props;

const result = [];
if (!selections?.length) {
return result;
}

const tagKeyItem = tagReport?.data?.find(item => item.key === tagKey);
selections?.map(selection => {
if (tagKeyItem?.values?.length) {
for (const item of tagKeyItem.values) {
if (item === selection) {
result.push(selection);
break;
}
}
}
});
return result;
}

private getTagValueOptions(): SelectWrapperOption[] {
const { tagKey, tagReport } = this.props;

Expand Down Expand Up @@ -112,7 +138,7 @@ class TagValueBase extends React.Component<TagValueProps, TagValueState> {
};

public render() {
const { intl, isDisabled, onTagValueInput, onTagValueSelect, selections, tagKeyValue } = this.props;
const { intl, isDisabled, onTagValueInput, onTagValueSelect, tagKeyValue } = this.props;

const selectOptions = this.getTagValueOptions();

Expand All @@ -125,7 +151,7 @@ class TagValueBase extends React.Component<TagValueProps, TagValueState> {
onSelect={onTagValueSelect}
options={selectOptions}
placeholder={intl.formatMessage(messages.chooseValuePlaceholder)}
selections={selections}
selections={this.getSelections()}
/>
);
}
Expand All @@ -145,7 +171,7 @@ class TagValueBase extends React.Component<TagValueProps, TagValueState> {
}

const mapStateToProps = createMapStateToProps<TagValueOwnProps, TagValueStateProps>(
(state, { router, tagKey, tagPathsType }) => {
(state, { endDate, router, startDate, tagKey, tagPathsType, timeScopeValue = -1 }) => {
const queryFromRoute = parseQuery<Query>(router.location.search);

const groupByOrgValue = getGroupByOrgValue(queryFromRoute);
Expand All @@ -154,10 +180,22 @@ const mapStateToProps = createMapStateToProps<TagValueOwnProps, TagValueStatePro

// Omitting key_only to share a single, cached request -- although the header doesn't need key values, the toolbar does
const tagQueryString = getQuery({
filter: {
key: tagKey,
},
...(startDate && endDate
? {
end_date: endDate,
filter: {
key: tagKey,
},
start_date: startDate,
}
: {
filter: {
key: tagKey,
time_scope_value: timeScopeValue,
},
}),
});

const tagReport = tagSelectors.selectTag(state, tagPathsType, tagType, tagQueryString);
const tagReportFetchStatus = tagSelectors.selectTagFetchStatus(state, tagPathsType, tagType, tagQueryString);

Expand Down
65 changes: 35 additions & 30 deletions src/routes/components/dataToolbar/utils/tags.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { ToolbarChipGroup } from '@patternfly/react-core';
import { ToolbarFilter, ToolbarItem } from '@patternfly/react-core';
import type { Query } from 'api/queries/query';
import type { Tag, TagPathsType } from 'api/tags/tag';
import { intl } from 'components/i18n';
import messages from 'locales/messages';
Expand Down Expand Up @@ -60,20 +59,20 @@ export const getTagKeySelect = ({
// For example, when switching the date range between current and previous months. Tags may only be available in the
// current month and vice versa.
//
// The problem is that we obtain a list of tag keys from the tag report, in order to show the currently applied filters
// using PatternFly filter chips. If an applied filter is not available in the tag report, then the associated
// filter chip will not be shown and users cannot clear that filter.
// The problem is that we obtain a list of tag keys from the tag report, in order to show the user's filters
// via PatternFly filter chips. If the user's filter is no longer available in the tag report, then the associated
// filter chip will not be shown and the user cannot clear that filter.
//
// As a workaround, we can use the filter_by query params to discover any missing tag keys. This represents the
// previously applied filters, which we combine with keys from the tag report.
// As a workaround, we can use active filters (i.e., obtained from query params) to discover any missing tag keys. Then,
// we can create a complete list of tag keys by combining previously applied filters with keys from the tag report.
export const getTagKeyOptions = (
tagReport: Tag,
query: Query,
filters: Filters,
isSelectWrapperOption = false
): ToolbarChipGroup[] | SelectWrapperOption[] => {
const options = [];
const reportOptions = getTagKeyOptionsFromReport(tagReport, isSelectWrapperOption);
const queryOptions = getTagKeyOptionsFromQuery(query, isSelectWrapperOption);
const filterOptions = getTagKeyOptionsFromFilters(filters, isSelectWrapperOption);

const isTagKeyEqual = (a, b) => {
if (isSelectWrapperOption) {
Expand All @@ -88,39 +87,36 @@ export const getTagKeyOptions = (
options.push(reportoption);
}
}
for (const queryOption of queryOptions) {
if (!options.find(option => isTagKeyEqual(option, queryOption))) {
options.push(queryOption);
for (const filterOption of filterOptions) {
if (!options.find(option => isTagKeyEqual(option, filterOption))) {
options.push(filterOption);
}
}
return options;
};

const getTagKeyOptionsFromQuery = (
query: Query,
const getTagKeyOptionsFromFilters = (
filter: Filters,
isSelectWrapperOption = false
): ToolbarChipGroup[] | SelectWrapperOption[] => {
const options = [];

if (!query?.filter_by) {
if (!filter?.tag) {
return options;
}

for (const filter of Object.keys(query.filter_by)) {
if (filter.indexOf(tagPrefix) !== -1) {
const key = filter.substring(tagPrefix.length);
options.push(
isSelectWrapperOption
? {
toString: () => key, // Tag keys not localized
value: key,
}
: {
key,
name: key, // Tag keys not localized
}
);
}
for (const key of Object.keys(filter.tag)) {
options.push(
isSelectWrapperOption
? {
toString: () => key, // Tag keys not localized
value: key,
}
: {
key,
name: key, // Tag keys not localized
}
);
}
return options;
};
Expand Down Expand Up @@ -178,27 +174,33 @@ const getTagKeyOptionsFromReport = (
export const getTagValueSelect = ({
currentCategory,
currentTagKey,
endDate,
filters,
isDisabled,
onDelete,
onTagValueSelect,
onTagValueInput,
onTagValueInputChange,
startDate,
tagKeyValueInput,
tagKeyOption,
tagPathsType,
timeScopeValue,
}: {
currentCategory?: string;
currentTagKey?: string;
endDate?: string;
filters?: Filters;
isDisabled?: boolean;
onDelete?: (type: any, chip: any) => void;
onTagValueSelect?: (event: any, selection) => void;
onTagValueInput?: (event: any) => void;
onTagValueInputChange?: (value: string) => void;
startDate?: string;
tagKeyValueInput?: string;
tagKeyOption?: ToolbarChipGroup;
tagPathsType?: TagPathsType;
timeScopeValue?: number;
}) => {
// Todo: categoryName workaround for https://issues.redhat.com/browse/COST-2094
const categoryName = {
Expand All @@ -215,14 +217,17 @@ export const getTagValueSelect = ({
showToolbarItem={currentCategory === tagKey && currentTagKey === tagKeyOption.key}
>
<TagValue
endDate={endDate}
isDisabled={isDisabled && !hasFilters(filters)}
onTagValueSelect={onTagValueSelect}
onTagValueInput={onTagValueInput}
onTagValueInputChange={onTagValueInputChange}
selections={filters?.tag?.[tagKeyOption.key] ? filters.tag[tagKeyOption.key].map(filter => filter.value) : []}
selections={filters?.tag?.[tagKeyOption.key]?.map(filter => filter.value)}
startDate={startDate}
tagKey={currentTagKey}
tagKeyValue={tagKeyValueInput}
tagPathsType={tagPathsType}
timeScopeValue={timeScopeValue}
/>
</ToolbarFilter>
);
Expand Down
9 changes: 3 additions & 6 deletions src/routes/components/groupBy/groupBy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -339,9 +339,8 @@ class GroupByBase extends React.Component<GroupByProps, GroupByState> {
}

const mapStateToProps = createMapStateToProps<GroupByOwnProps, GroupByStateProps>(
(state, { endDate, orgPathsType, resourcePathsType, startDate, tagPathsType, timeScopeValue }) => {
// Use start and end dates with Cost Explorer
// Default to current month filter for details pages
(state, { endDate, orgPathsType, resourcePathsType, startDate, tagPathsType, timeScopeValue = -1 }) => {
// Use start and end dates with Cost Explorer, default to current month filter for details pages
const tagFilter =
startDate && endDate
? {
Expand All @@ -350,9 +349,7 @@ const mapStateToProps = createMapStateToProps<GroupByOwnProps, GroupByStateProps
}
: {
filter: {
resolution: 'monthly',
time_scope_units: 'month',
time_scope_value: timeScopeValue !== undefined ? timeScopeValue : -1,
time_scope_value: timeScopeValue,
},
};

Expand Down
2 changes: 1 addition & 1 deletion src/routes/details/awsBreakdown/awsBreakdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const mapStateToProps = createMapStateToProps<AwsBreakdownOwnProps, BreakdownSta
filter: {
resolution: 'monthly',
time_scope_units: 'month',
time_scope_value: timeScopeValue !== undefined ? timeScopeValue : -1,
time_scope_value: timeScopeValue,
},
filter_by: {
// Add filters here to apply logical OR/AND
Expand Down
2 changes: 1 addition & 1 deletion src/routes/details/awsBreakdown/instances/instances.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ const useMapToProps = ({ costType, currency, query }): InstancesStateProps => {
...(query.filter || baseQuery.filter),
resolution: 'monthly',
time_scope_units: 'month',
time_scope_value: timeScopeValue !== undefined ? timeScopeValue : -1,
time_scope_value: timeScopeValue,
},
filter_by: {
// Add filters here to apply logical OR/AND
Expand Down
13 changes: 4 additions & 9 deletions src/routes/details/awsBreakdown/instances/instancesToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,22 +187,17 @@ export class InstancesToolbarBase extends React.Component<InstancesToolbarProps,
}

const mapStateToProps = createMapStateToProps<InstancesToolbarOwnProps, InstancesToolbarStateProps>(
(state, { timeScopeValue }) => {
(state, { timeScopeValue = -1 }) => {
// Note: Omitting key_only would help to share a single, cached request. Only the toolbar requires key values;
// however, for better server-side performance, we chose to use key_only here.
const baseQuery = {
const tagQueryString = getQuery({
filter: {
resolution: 'monthly',
time_scope_units: 'month',
time_scope_value: timeScopeValue !== undefined ? timeScopeValue : -1,
time_scope_value: timeScopeValue,
},
key_only: true,
limit: 1000,
};

const tagQueryString = getQuery({
...baseQuery,
});

const tagReport = tagSelectors.selectTag(state, tagPathsType, tagType, tagQueryString);
const tagReportFetchStatus = tagSelectors.selectTagFetchStatus(state, tagPathsType, tagType, tagQueryString);

Expand Down
Loading
Loading