diff --git a/locales/data.json b/locales/data.json index 49f28cbaa..3ecc596bc 100644 --- a/locales/data.json +++ b/locales/data.json @@ -1488,12 +1488,6 @@ "value": "Sources table" } ], - "costModelsSourceType": [ - { - "type": 0, - "value": "Source type" - } - ], "costModelsTableAriaLabel": [ { "type": 0, @@ -2736,6 +2730,14 @@ } ] }, + "name": { + "value": [ + { + "type": 0, + "value": "Name" + } + ] + }, "node": { "value": [ { @@ -2811,6 +2813,22 @@ } ] }, + "source_type": { + "value": [ + { + "type": 0, + "value": "Source type" + } + ] + }, + "status": { + "value": [ + { + "type": 0, + "value": "Status" + } + ] + }, "subscription_guid": { "value": [ { @@ -3256,6 +3274,12 @@ "value": "value" } ], + "disabled": [ + { + "type": 0, + "value": "Disabled" + } + ], "discountMinus": [ { "type": 0, @@ -3482,6 +3506,12 @@ "value": "No match found" } ], + "enabled": [ + { + "type": 0, + "value": "Enabled" + } + ], "end": [ { "type": 0, @@ -11260,6 +11290,12 @@ "value": "dateRange" } ], + "sourceType": [ + { + "type": 0, + "value": "Source type" + } + ], "sources": [ { "type": 0, diff --git a/locales/translations.json b/locales/translations.json index ecf692d39..4c3e694a9 100644 --- a/locales/translations.json +++ b/locales/translations.json @@ -125,7 +125,6 @@ "costModelsSourceEmptyStateDesc": "Select the source(s) you want to apply this cost model to.", "costModelsSourceEmptyStateTitle": "No sources are assigned", "costModelsSourceTableAriaLabel": "Sources table", - "costModelsSourceType": "Source type", "costModelsTableAriaLabel": "Cost models table", "costModelsTagRateTableKey": "Tag key", "costModelsTagRateTableValue": "Tag value", @@ -209,7 +208,7 @@ "detailsCostValue": "Cost: {value}", "detailsEmptyState": "Processing data to generate a list of all services that sums to a total cost...", "detailsMoreClusters": ", {value} more...", - "detailsResourceNames": "{value, select, account {Account names} aws_category {Cost category names} cluster {Cluster names} gcp_project {GCP project names} node {Node names} org_unit_id {Organizational unit names} payer_tenant_id {Account names} product_service {Service names} project {Project names} region {Region names} resource_location {Region names} service {Service names} service_name {Service names} subscription_guid {Account names} tag {Tag names} other {}}", + "detailsResourceNames": "{value, select, account {Account names} aws_category {Cost category names} cluster {Cluster names} gcp_project {GCP project names} name {Name} node {Node names} org_unit_id {Organizational unit names} payer_tenant_id {Account names} product_service {Service names} project {Project names} region {Region names} resource_location {Region names} service {Service names} service_name {Service names} status {Status} subscription_guid {Account names} source_type {Source type} tag {Tag names} other {}}", "detailsSummaryModalTitle": "{groupBy, select, account {{name} accounts} aws_category {{name} cost categories} cluster {{name} clusters} gcp_project {{name} GCP projects} node {{name} nodes} org_unit_id {{name} organizational units} payer_tenant_id {{name} accounts} product_service {{name} services} project {{name} projects} region {{name} regions} resource_location {{name} regions} service {{name} services} service_name {{name} services} subscription_guid {{name} accounts} tag {{name} tags} other {}}", "detailsUnusedRequestsLabel": "Unrequested capacity", "detailsUnusedUnits": "{units} ({percentage}% of capacity)", @@ -219,6 +218,7 @@ "detailsUsageRequests": "Requests - {value} {units}", "detailsUsageUsage": "Usage - {value} {units}", "detailsViewAll": "{value, select, account {View all accounts} aws_category {View all cost categories} cluster {View all clusters} gcp_project {View all GCP projects} node {View all nodes} org_unit_id {View all organizational units} payer_tenant_id {View all accounts} product_service {View all services} project {View all projects} region {View all regions} resource_location {View all regions} service {View all Services} service_name {View all services} subscription_guid {View all accounts} tag {View all tags} other {}}", + "disabled": "Disabled", "discountMinus": "Discount (-)", "distribute": "Distribute", "distributeCosts": "{value, select, true {Distribute {type, select, platform {platform} worker {worker} other {}} unallocated capacity}false {Do not distribute {type, select, platform {platform} worker {worker} other {}} unallocated capacity}other {}}", @@ -240,6 +240,7 @@ "emptyFilterSourceStateSubtitle": "Sorry, no source with the given filter was found.", "emptyFilterStateSubtitle": "Sorry, no data with the given filter was found.", "emptyFilterStateTitle": "No match found", + "enabled": "Enabled", "end": "End", "equalsSymbol": "=", "errorStateNotAuthorizedDesc": "Contact the cost management administrator to provide access to this application", @@ -497,6 +498,7 @@ "settingsSuccessTitle": "Application settings saved", "settingsTitle": "Cost Management Settings", "sinceDate": "{dateRange}", + "sourceType": "Source type", "sources": "Sources", "start": "Start", "status": "{value, select, pending {Pending} running {Running} failed {Failed} other {}}", diff --git a/src/locales/messages.ts b/src/locales/messages.ts index 64bbe6b1e..1f297758b 100644 --- a/src/locales/messages.ts +++ b/src/locales/messages.ts @@ -692,11 +692,6 @@ export default defineMessages({ description: 'Sources table', id: 'costModelsSourceTableAriaLabel', }, - costModelsSourceType: { - defaultMessage: 'Source type', - description: 'Source type', - id: 'costModelsSourceType', - }, costModelsTableAriaLabel: { defaultMessage: 'Cost models table', description: 'Cost models table', @@ -1202,6 +1197,7 @@ export default defineMessages({ 'aws_category {Cost category names} ' + 'cluster {Cluster names} ' + 'gcp_project {GCP project names} ' + + 'name {Name} ' + 'node {Node names} ' + 'org_unit_id {Organizational unit names} ' + 'payer_tenant_id {Account names} ' + @@ -1211,7 +1207,9 @@ export default defineMessages({ 'resource_location {Region names} ' + 'service {Service names} ' + 'service_name {Service names} ' + + 'status {Status} ' + 'subscription_guid {Account names} ' + + 'source_type {Source type} ' + 'tag {Tag names} ' + 'other {}}', description: 'Details table resource names', @@ -1296,6 +1294,11 @@ export default defineMessages({ description: 'View all {value}', id: 'detailsViewAll', }, + disabled: { + defaultMessage: 'Disabled', + description: 'Disabled', + id: 'disabled', + }, discountMinus: { defaultMessage: 'Discount (-)', description: 'Discount (-)', @@ -1421,6 +1424,11 @@ export default defineMessages({ description: 'No match found', id: 'emptyFilterStateTitle', }, + enabled: { + defaultMessage: 'Enabled', + description: 'Enabled', + id: 'enabled', + }, end: { defaultMessage: 'End', description: 'End', @@ -3065,6 +3073,11 @@ export default defineMessages({ description: 'Jan 1-31', id: 'sinceDate', }, + sourceType: { + defaultMessage: 'Source type', + description: 'Source type', + id: 'sourceType', + }, sources: { defaultMessage: 'Sources', description: 'Sources', diff --git a/src/routes/settings/settings.tsx b/src/routes/settings/settings.tsx index a4b71a9c7..746d8e7a2 100644 --- a/src/routes/settings/settings.tsx +++ b/src/routes/settings/settings.tsx @@ -9,7 +9,8 @@ import type { WrappedComponentProps } from 'react-intl'; import { injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { Calculations } from 'routes/settings/calculations'; -import { CostModelsDetails } from 'routes/settings/costModelsDetails'; +import { CostModelsDetails } from 'routes/settings/costModels'; +import { TagDetails } from 'routes/settings/tagDetails'; import { createMapStateToProps } from 'store/common'; import type { RouterComponentProps } from 'utils/router'; import { withRouter } from 'utils/router'; @@ -136,7 +137,7 @@ class SettingsBase extends React.Component { } else if (currentTab === SettingsTab.calculations) { return ; } else if (currentTab === SettingsTab.tags) { - return null; + return ; } else { return emptyTab; } diff --git a/src/routes/settings/tagDetails/index.ts b/src/routes/settings/tagDetails/index.ts new file mode 100644 index 000000000..6c0e51727 --- /dev/null +++ b/src/routes/settings/tagDetails/index.ts @@ -0,0 +1 @@ +export { default as TagDetails } from './tagDetails'; diff --git a/src/routes/settings/tagDetails/tagDetails.styles.ts b/src/routes/settings/tagDetails/tagDetails.styles.ts new file mode 100644 index 000000000..84f8446d2 --- /dev/null +++ b/src/routes/settings/tagDetails/tagDetails.styles.ts @@ -0,0 +1,31 @@ +import global_BackgroundColor_light_100 from '@patternfly/react-tokens/dist/js/global_BackgroundColor_light_100'; +import global_spacer_lg from '@patternfly/react-tokens/dist/js/global_spacer_lg'; +import global_spacer_md from '@patternfly/react-tokens/dist/js/global_spacer_md'; +import type React from 'react'; + +export const styles = { + content: { + paddingBottom: global_spacer_lg.value, + paddingTop: global_spacer_lg.value, + }, + paginationContainer: { + marginLeft: global_spacer_lg.value, + marginRight: global_spacer_lg.value, + }, + pagination: { + backgroundColor: global_BackgroundColor_light_100.value, + paddingBottom: global_spacer_md.value, + paddingTop: global_spacer_md.value, + }, + tableContainer: { + marginLeft: global_spacer_lg.value, + marginRight: global_spacer_lg.value, + }, + tagDetails: { + minHeight: '100vh', + }, + toolbarContainer: { + marginLeft: global_spacer_lg.value, + marginRight: global_spacer_lg.value, + }, +} as { [className: string]: React.CSSProperties }; diff --git a/src/routes/settings/tagDetails/tagDetails.tsx b/src/routes/settings/tagDetails/tagDetails.tsx new file mode 100644 index 000000000..43e806114 --- /dev/null +++ b/src/routes/settings/tagDetails/tagDetails.tsx @@ -0,0 +1,350 @@ +import { Pagination, PaginationVariant } from '@patternfly/react-core'; +import type { OcpQuery } from 'api/queries/ocpQuery'; +import { getQuery, parseQuery } from 'api/queries/ocpQuery'; +import type { OcpReport } from 'api/reports/ocpReports'; +import { ReportPathsType, ReportType } from 'api/reports/report'; +import type { AxiosError } from 'axios'; +import messages from 'locales/messages'; +import React from 'react'; +import type { WrappedComponentProps } from 'react-intl'; +import { injectIntl } from 'react-intl'; +import { connect } from 'react-redux'; +import { Loading } from 'routes/state/loading'; +import { NotAvailable } from 'routes/state/notAvailable'; +import { ComputedReportItemValueType } from 'routes/views/components/charts/common'; +import type { ColumnManagementModalOption } from 'routes/views/details/components/columnManagement'; +import { initHiddenColumns } from 'routes/views/details/components/columnManagement'; +import { getGroupById, getGroupByTagKey } from 'routes/views/utils/groupBy'; +import { + handleOnFilterAdded, + handleOnFilterRemoved, + handleOnPerPageSelect, + handleOnSetPage, + handleOnSort, +} from 'routes/views/utils/handles'; +import { createMapStateToProps, FetchStatus } from 'store/common'; +import { featureFlagsSelectors } from 'store/featureFlags'; +import { reportActions, reportSelectors } from 'store/reports'; +import { getIdKeyForGroupBy } from 'utils/computedReport/getComputedOcpReportItems'; +import type { ComputedReportItem } from 'utils/computedReport/getComputedReportItems'; +import { getUnsortedComputedReportItems } from 'utils/computedReport/getComputedReportItems'; +import { getCostDistribution, getCurrency } from 'utils/localStorage'; +import { tagPrefix } from 'utils/props'; +import type { RouterComponentProps } from 'utils/router'; +import { withRouter } from 'utils/router'; + +import { styles } from './tagDetails.styles'; +import { DetailsTableColumnIds, TagTable } from './tagTable'; +import { TagToolbar } from './tagToolbar'; + +export interface OcpDetailsStateProps { + query: OcpQuery; + report: OcpReport; + reportError: AxiosError; + reportFetchStatus: FetchStatus; + reportQueryString: string; +} + +interface OcpDetailsDispatchProps { + fetchReport: typeof reportActions.fetchReport; +} + +interface OcpDetailsState { + columns?: any[]; + hiddenColumns?: Set; + isAllSelected?: boolean; + rows?: any[]; + selectedItems?: ComputedReportItem[]; +} + +type OcpDetailsOwnProps = RouterComponentProps & WrappedComponentProps; + +type OcpDetailsProps = OcpDetailsStateProps & OcpDetailsOwnProps & OcpDetailsDispatchProps; + +const baseQuery: OcpQuery = { + filter: { + limit: 10, + offset: 0, + }, + filter_by: {}, + exclude: {}, + group_by: { + project: '*', + }, + order_by: { + cost: 'desc', + }, +}; + +const defaultColumnOptions: ColumnManagementModalOption[] = [ + { label: messages.monthOverMonthChange, value: DetailsTableColumnIds.monthOverMonth }, + { + description: messages.ocpDetailsInfrastructureCostDesc, + label: messages.ocpDetailsInfrastructureCost, + value: DetailsTableColumnIds.infrastructure, + hidden: true, + }, + { + description: messages.ocpDetailsSupplementaryCostDesc, + label: messages.ocpDetailsSupplementaryCost, + value: DetailsTableColumnIds.supplementary, + hidden: true, + }, +]; + +const reportType = ReportType.cost; +const reportPathsType = ReportPathsType.ocp; + +class TagDetails extends React.Component { + protected defaultState: OcpDetailsState = { + columns: [], + hiddenColumns: initHiddenColumns(defaultColumnOptions), + isAllSelected: false, + rows: [], + selectedItems: [], + }; + public state: OcpDetailsState = { ...this.defaultState }; + + constructor(stateProps, dispatchProps) { + super(stateProps, dispatchProps); + this.handleBulkSelected = this.handleBulkSelected.bind(this); + this.handleSelected = this.handleSelected.bind(this); + } + + public componentDidMount() { + this.updateReport(); + } + + public componentDidUpdate(prevProps: OcpDetailsProps, prevState: OcpDetailsState) { + const { report, reportError, reportQueryString, router } = this.props; + const { selectedItems } = this.state; + + const newQuery = prevProps.reportQueryString !== reportQueryString; + const noReport = !report && !reportError; + const noLocation = !router.location.search; + const newItems = prevState.selectedItems !== selectedItems; + + if (newQuery || noReport || noLocation || newItems) { + this.updateReport(); + } + } + + private getComputedItems = () => { + const { query, report } = this.props; + + const groupById = getIdKeyForGroupBy(query.group_by); + const groupByTagKey = getGroupByTagKey(query); + + return getUnsortedComputedReportItems({ + report, + idKey: (groupByTagKey as any) || groupById, + }); + }; + + private getPagination = (isDisabled = false, isBottom = false) => { + const { intl, query, report, router } = this.props; + + const count = report && report.meta ? report.meta.count : 0; + const limit = + report && report.meta && report.meta.filter && report.meta.filter.limit + ? report.meta.filter.limit + : baseQuery.filter.limit; + const offset = + report && report.meta && report.meta.filter && report.meta.filter.offset + ? report.meta.filter.offset + : baseQuery.filter.offset; + const page = Math.trunc(offset / limit + 1); + + return ( + handleOnPerPageSelect(query, router, perPage)} + onSetPage={(event, pageNumber) => handleOnSetPage(query, router, report, pageNumber)} + page={page} + perPage={limit} + titles={{ + paginationTitle: intl.formatMessage(messages.paginationTitle, { + title: intl.formatMessage(messages.openShift), + placement: isBottom ? 'bottom' : 'top', + }), + }} + variant={isBottom ? PaginationVariant.bottom : PaginationVariant.top} + widgetId={`exports-pagination${isBottom ? '-bottom' : ''}`} + /> + ); + }; + + private getTable = () => { + const { query, report, reportFetchStatus, reportQueryString, router } = this.props; + const { hiddenColumns, isAllSelected, selectedItems } = this.state; + + const groupById = getIdKeyForGroupBy(query.group_by); + const groupByTagKey = getGroupByTagKey(query); + + return ( + handleOnSort(query, router, sortType, isSortAscending)} + report={report} + reportQueryString={reportQueryString} + selectedItems={selectedItems} + /> + ); + }; + + private getToolbar = (computedItems: ComputedReportItem[]) => { + const { query, report, router } = this.props; + const { isAllSelected, selectedItems } = this.state; + + const isDisabled = computedItems.length === 0; + const itemsTotal = report && report.meta ? report.meta.count : 0; + + return ( + handleOnFilterAdded(query, router, filter)} + onFilterRemoved={filter => handleOnFilterRemoved(query, router, filter)} + pagination={this.getPagination(isDisabled)} + query={query} + selectedItems={selectedItems} + /> + ); + }; + + private handleBulkSelected = (action: string) => { + const { isAllSelected } = this.state; + + if (action === 'none') { + this.setState({ isAllSelected: false, selectedItems: [] }); + } else if (action === 'page') { + this.setState({ + isAllSelected: false, + selectedItems: this.getComputedItems(), + }); + } else if (action === 'all') { + this.setState({ isAllSelected: !isAllSelected, selectedItems: [] }); + } + }; + + private handleSelected = (items: ComputedReportItem[], isSelected: boolean = false) => { + const { isAllSelected, selectedItems } = this.state; + + let newItems = [...(isAllSelected ? this.getComputedItems() : selectedItems)]; + if (items && items.length > 0) { + if (isSelected) { + items.map(item => newItems.push(item)); + } else { + items.map(item => { + newItems = newItems.filter(val => val.id !== item.id); + }); + } + } + this.setState({ isAllSelected: false, selectedItems: newItems }); + }; + + private updateReport = () => { + const { fetchReport, reportQueryString } = this.props; + fetchReport(reportPathsType, reportType, reportQueryString); + }; + + public render() { + const { intl, reportError, reportFetchStatus } = this.props; + + const computedItems = this.getComputedItems(); + const isDisabled = computedItems.length === 0; + const title = intl.formatMessage(messages.ocpDetailsTitle); + + // Note: Providers are fetched via the AccountSettings component used by all routes + if (reportError) { + return ; + } + return ( +
+
+
{this.getToolbar(computedItems)}
+ {reportFetchStatus === FetchStatus.inProgress ? ( + + ) : ( + <> +
{this.getTable()}
+
+
{this.getPagination(isDisabled, true)}
+
+ + )} +
+
+ ); + } +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const mapStateToProps = createMapStateToProps((state, { router }) => { + const queryFromRoute = parseQuery(router.location.search); + const groupBy = queryFromRoute.group_by ? getGroupById(queryFromRoute) : getGroupById(baseQuery); + + const isCostDistributionFeatureEnabled = featureFlagsSelectors.selectIsCostDistributionFeatureEnabled(state); + const costDistribution = + groupBy === 'project' && isCostDistributionFeatureEnabled ? getCostDistribution() : undefined; + const currency = getCurrency(); + + const query: any = { + ...baseQuery, + ...(costDistribution === ComputedReportItemValueType.distributed && { + order_by: { + distributed_cost: 'desc', + }, + }), + ...queryFromRoute, + }; + const reportQuery = { + category: query.category, + currency, + delta: costDistribution === ComputedReportItemValueType.distributed ? 'distributed_cost' : 'cost', + exclude: query.exclude, + filter: { + ...query.filter, + resolution: 'monthly', + time_scope_units: 'month', + time_scope_value: -1, + }, + filter_by: query.filter_by, + group_by: query.group_by, + order_by: query.order_by, + }; + + const reportQueryString = getQuery(reportQuery); + const report = reportSelectors.selectReport(state, reportPathsType, reportType, reportQueryString); + const reportError = reportSelectors.selectReportError(state, reportPathsType, reportType, reportQueryString); + const reportFetchStatus = reportSelectors.selectReportFetchStatus( + state, + reportPathsType, + reportType, + reportQueryString + ); + + return { + query, + report, + reportError, + reportFetchStatus, + reportQueryString, + }; +}); + +const mapDispatchToProps: OcpDetailsDispatchProps = { + fetchReport: reportActions.fetchReport, +}; + +export default injectIntl(withRouter(connect(mapStateToProps, mapDispatchToProps)(TagDetails))); diff --git a/src/routes/settings/tagDetails/tagTable.tsx b/src/routes/settings/tagDetails/tagTable.tsx new file mode 100644 index 000000000..00c945a16 --- /dev/null +++ b/src/routes/settings/tagDetails/tagTable.tsx @@ -0,0 +1,146 @@ +import 'routes/views/details/components/dataTable/dataTable.scss'; + +import { Label } from '@patternfly/react-core'; +import type { Report, ReportItem } from 'api/reports/report'; +import messages from 'locales/messages'; +import React from 'react'; +import type { WrappedComponentProps } from 'react-intl'; +import { injectIntl } from 'react-intl'; +import { DataTable } from 'routes/views/details/components/dataTable'; +import type { ComputedReportItem } from 'utils/computedReport/getComputedReportItems'; +import { getUnsortedComputedReportItems } from 'utils/computedReport/getComputedReportItems'; +import type { RouterComponentProps } from 'utils/router'; +import { withRouter } from 'utils/router'; + +interface DetailsTableOwnProps extends RouterComponentProps, WrappedComponentProps { + isAllSelected?: boolean; + isLoading?: boolean; + onSelected(items: ComputedReportItem[], isSelected: boolean); + onSort(value: string, isSortAscending: boolean); + report: Report; + reportQueryString: string; + selectedItems?: ComputedReportItem[]; +} + +interface DetailsTableState { + columns?: any[]; + rows?: any[]; +} + +type DetailsTableProps = DetailsTableOwnProps; + +export const DetailsTableColumnIds = { + infrastructure: 'infrastructure', + monthOverMonth: 'monthOverMonth', + supplementary: 'supplementary', +}; + +class DetailsTableBase extends React.Component { + public state: DetailsTableState = { + columns: [], + rows: [], + }; + + public componentDidMount() { + this.initDatum(); + } + + public componentDidUpdate(prevProps: DetailsTableProps) { + const { report, selectedItems } = this.props; + const currentReport = report && report.data ? JSON.stringify(report.data) : ''; + const previousReport = prevProps.report && prevProps.report.data ? JSON.stringify(prevProps.report.data) : ''; + + if (previousReport !== currentReport || prevProps.selectedItems !== selectedItems) { + this.initDatum(); + } + } + + private initDatum = () => { + const { intl, isAllSelected, report, selectedItems } = this.props; + if (!report) { + return; + } + + const rows = []; + const computedItems = getUnsortedComputedReportItems({ + report, + idKey: 'project' as any, + }); + + const columns = [ + { + name: '', // Selection column + }, + { + orderBy: 'name', + name: intl.formatMessage(messages.detailsResourceNames, { value: 'name' }), + ...(computedItems.length && { isSortable: true }), + }, + { + orderBy: 'status', + name: intl.formatMessage(messages.detailsResourceNames, { value: 'status' }), + ...(computedItems.length && { isSortable: true }), + }, + { + orderBy: 'source_type', + name: intl.formatMessage(messages.sourceType), + ...(computedItems.length && { isSortable: true }), + }, + ]; + + computedItems.map((item, index) => { + const label = item && item.label !== null ? item.label : ''; + + rows.push({ + cells: [ + {}, // Empty cell for row selection + { + value: label, + }, + { + value: ( + + ), + }, + { value: 'source type' }, + ], + item, + selected: isAllSelected || (selectedItems && selectedItems.find(val => val.id === item.id) !== undefined), + }); + }); + + const filteredColumns = (columns as any[]).filter(column => !column.hidden); + const filteredRows = rows.map(({ ...row }) => { + row.cells = row.cells.filter(cell => !cell.hidden); + return row; + }); + + this.setState({ + columns: filteredColumns, + rows: filteredRows, + }); + }; + + public render() { + const { isLoading, onSelected, onSort, selectedItems } = this.props; + const { columns, rows } = this.state; + + return ( + + ); + } +} + +const TagTable = injectIntl(withRouter(DetailsTableBase)); + +export { TagTable }; +export type { DetailsTableProps }; diff --git a/src/routes/settings/tagDetails/tagToolbar.tsx b/src/routes/settings/tagDetails/tagToolbar.tsx new file mode 100644 index 000000000..f22a21668 --- /dev/null +++ b/src/routes/settings/tagDetails/tagToolbar.tsx @@ -0,0 +1,179 @@ +import type { ToolbarChipGroup } from '@patternfly/react-core'; +import type { OcpQuery } from 'api/queries/ocpQuery'; +import { getQuery } from 'api/queries/ocpQuery'; +import { ResourcePathsType } from 'api/resources/resource'; +import type { OcpTag } from 'api/tags/ocpTags'; +import { TagPathsType, TagType } from 'api/tags/tag'; +import messages from 'locales/messages'; +import React from 'react'; +import type { WrappedComponentProps } from 'react-intl'; +import { injectIntl } from 'react-intl'; +import { connect } from 'react-redux'; +import { DataToolbar } from 'routes/views/components/dataToolbar'; +import type { Filter } from 'routes/views/utils/filter'; +import type { FetchStatus } from 'store/common'; +import { createMapStateToProps } from 'store/common'; +import { tagActions, tagSelectors } from 'store/tags'; +import type { ComputedReportItem } from 'utils/computedReport/getComputedReportItems'; +import { isEqual } from 'utils/equal'; +import { tagKey } from 'utils/props'; + +interface DetailsToolbarOwnProps { + isAllSelected?: boolean; + isDisabled?: boolean; + itemsPerPage?: number; + itemsTotal?: number; + onBulkSelected(action: string); + onFilterAdded(filter: Filter); + onFilterRemoved(filter: Filter); + pagination?: React.ReactNode; + query?: OcpQuery; + selectedItems?: ComputedReportItem[]; +} + +interface DetailsToolbarStateProps { + tagReport?: OcpTag; + tagReportFetchStatus?: FetchStatus; + tagQueryString?: string; +} + +interface DetailsToolbarDispatchProps { + fetchTag?: typeof tagActions.fetchTag; +} + +interface DetailsToolbarState { + categoryOptions?: ToolbarChipGroup[]; +} + +type DetailsToolbarProps = DetailsToolbarOwnProps & + DetailsToolbarStateProps & + DetailsToolbarDispatchProps & + WrappedComponentProps; + +const tagType = TagType.tag; +const tagPathsType = TagPathsType.ocp; + +export class DetailsToolbarBase extends React.Component { + protected defaultState: DetailsToolbarState = {}; + public state: DetailsToolbarState = { ...this.defaultState }; + + public componentDidMount() { + this.setState( + { + categoryOptions: this.getCategoryOptions(), + }, + () => { + this.updateReport(); + } + ); + } + + public componentDidUpdate(prevProps: DetailsToolbarProps) { + const { query, tagReport } = this.props; + if (!isEqual(tagReport, prevProps.tagReport)) { + this.setState( + { + categoryOptions: this.getCategoryOptions(), + }, + () => { + this.updateReport(); + } + ); + } else if (query && !isEqual(query, prevProps.query)) { + this.updateReport(); + } + } + + private getCategoryOptions = (): ToolbarChipGroup[] => { + const { intl, tagReport } = this.props; + + const options = [ + { name: intl.formatMessage(messages.filterByValues, { value: 'cluster' }), key: 'cluster' }, + { name: intl.formatMessage(messages.filterByValues, { value: 'node' }), key: 'node' }, + { name: intl.formatMessage(messages.filterByValues, { value: 'project' }), key: 'project' }, + ]; + + if (tagReport && tagReport.data && tagReport.data.length) { + options.push({ + name: intl.formatMessage(messages.filterByValues, { value: tagKey }), + key: tagKey, + }); + } + return options; + }; + + private updateReport = () => { + const { fetchTag, tagQueryString } = this.props; + fetchTag(tagPathsType, tagType, tagQueryString); + }; + + public render() { + const { + isAllSelected, + isDisabled, + itemsPerPage, + itemsTotal, + onBulkSelected, + onFilterAdded, + onFilterRemoved, + pagination, + query, + selectedItems, + tagReport, + } = this.props; + const { categoryOptions } = this.state; + + return ( + + ); + } +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const mapStateToProps = createMapStateToProps((state, props) => { + // Note: Omitting key_only would help to share a single, cached request -- the toolbar requires key values + // However, for better server-side performance, we chose to use key_only here. + const tagQueryString = getQuery({ + filter: { + resolution: 'monthly', + time_scope_units: 'month', + time_scope_value: -1, + }, + key_only: true, + limit: 1000, + }); + const tagReport = tagSelectors.selectTag(state, tagPathsType, tagType, tagQueryString); + const tagReportFetchStatus = tagSelectors.selectTagFetchStatus(state, tagPathsType, tagType, tagQueryString); + return { + tagReport, + tagReportFetchStatus, + tagQueryString, + }; +}); + +const mapDispatchToProps: DetailsToolbarDispatchProps = { + fetchTag: tagActions.fetchTag, +}; + +const DetailsToolbarConnect = connect(mapStateToProps, mapDispatchToProps)(DetailsToolbarBase); +const TagToolbar = injectIntl(DetailsToolbarConnect); + +export { TagToolbar }; +export type { DetailsToolbarProps };