diff --git a/locales/data.json b/locales/data.json index 80fabf3f3..effa863db 100644 --- a/locales/data.json +++ b/locales/data.json @@ -10205,6 +10205,36 @@ "value": "There are no export files available" } ], + "noMappedTags": [ + { + "type": 0, + "value": "No mapped tags" + } + ], + "noMappedTagsDesc": [ + { + "type": 0, + "value": "Map multiple tags across data sources to be used as a single tag key for report grouping and filtering. " + }, + { + "type": 1, + "value": "warning" + }, + { + "type": 0, + "value": " Changes will be reflected within 24 hours. " + }, + { + "type": 1, + "value": "learnMore" + } + ], + "noMappedTagsWarning": [ + { + "type": 0, + "value": "Tags must be enabled to be mapped." + } + ], "noOptimizationsDesc": [ { "type": 0, diff --git a/locales/translations.json b/locales/translations.json index d3f1cd710..08ef744cf 100644 --- a/locales/translations.json +++ b/locales/translations.json @@ -374,6 +374,9 @@ "noDataStateRefresh": "Refresh this page", "noDataStateTitle": "Still processing the data", "noExportsStateTitle": "There are no export files available", + "noMappedTags": "No mapped tags", + "noMappedTagsDesc": "Map multiple tags across data sources to be used as a single tag key for report grouping and filtering. {warning} Changes will be reflected within 24 hours. {learnMore}", + "noMappedTagsWarning": "Tags must be enabled to be mapped.", "noOptimizationsDesc": "Resource Optimization is now available in preview for select customers. If your organization wants to participate, tell us through the Feedback button, which is purple and located on the right. Otherwise, there is not enough data available to generate an optimization.", "noOptimizationsTitle": "No optimizations available", "noProvidersStateAwsDesc": "Add an Amazon Web Services account to see a total cost breakdown of your spend by accounts, organizational units, services, regions, or tags.", diff --git a/src/api/settings.ts b/src/api/settings.ts index 372dfe060..e3ab737ad 100644 --- a/src/api/settings.ts +++ b/src/api/settings.ts @@ -40,6 +40,7 @@ export const enum SettingsType { tags = 'tags', tagsEnable = 'tagsEnable', tagsDisable = 'tagsDisable', + tagsMappings = 'tagsMappings', } export const SettingsTypePaths: Partial> = { @@ -52,6 +53,7 @@ export const SettingsTypePaths: Partial> = { [SettingsType.tags]: 'settings/tags', [SettingsType.tagsEnable]: 'settings/tags/enable/', [SettingsType.tagsDisable]: 'settings/tags/disable/', + [SettingsType.tagsMappings]: 'settings/tags/mappings', }; export function fetchSettings(settingsType: SettingsType, query: string) { diff --git a/src/locales/messages.ts b/src/locales/messages.ts index aa6ee0567..c605d0716 100644 --- a/src/locales/messages.ts +++ b/src/locales/messages.ts @@ -2418,6 +2418,23 @@ export default defineMessages({ description: 'There are no export files available', id: 'noExportsStateTitle', }, + noMappedTags: { + defaultMessage: 'No mapped tags', + description: 'No mapped tags', + id: 'noMappedTags', + }, + noMappedTagsDesc: { + defaultMessage: + 'Map multiple tags across data sources to be used as a single tag key for report grouping and filtering. {warning} Changes will be reflected within 24 hours. {learnMore}', + description: + 'Map multiple tags across data sources to be used as a single tag key for report grouping and filtering. {warning} Changes will be reflected within 24 hours. {learnMore}', + id: 'noMappedTagsDesc', + }, + noMappedTagsWarning: { + defaultMessage: 'Tags must be enabled to be mapped.', + description: 'Tags must be enabled to be mapped.', + id: 'noMappedTagsWarning', + }, noOptimizationsDesc: { defaultMessage: 'Resource Optimization is now available in preview for select customers. If your organization wants to participate, tell us through the Feedback button, which is purple and located on the right. Otherwise, there is not enough data available to generate an optimization.', diff --git a/src/routes/components/page/noData/noDataState.tsx b/src/routes/components/page/noData/noDataState.tsx index 6912fff87..1a3c05911 100644 --- a/src/routes/components/page/noData/noDataState.tsx +++ b/src/routes/components/page/noData/noDataState.tsx @@ -26,7 +26,7 @@ class NoDataStateBase extends React.Component { return ( {intl.formatMessage(messages.noDataStateTitle)}} + titleText={intl.formatMessage(messages.noDataStateTitle)} icon={} headingLevel="h5" /> diff --git a/src/routes/components/page/noOptimizations/noOptimizationsState.tsx b/src/routes/components/page/noOptimizations/noOptimizationsState.tsx index 771c8275f..4b78a7148 100644 --- a/src/routes/components/page/noOptimizations/noOptimizationsState.tsx +++ b/src/routes/components/page/noOptimizations/noOptimizationsState.tsx @@ -24,7 +24,7 @@ class NoOptimizationsStateBase extends React.Component {intl.formatMessage(messages.noOptimizationsTitle)}} + titleText={intl.formatMessage(messages.noOptimizationsTitle)} icon={} headingLevel="h1" /> diff --git a/src/routes/components/page/noProviders/noProvidersState.tsx b/src/routes/components/page/noProviders/noProvidersState.tsx index 175364ee1..c944ef6c5 100644 --- a/src/routes/components/page/noProviders/noProvidersState.tsx +++ b/src/routes/components/page/noProviders/noProvidersState.tsx @@ -84,7 +84,7 @@ class NoProvidersStateBase extends React.Component { return ( {intl.formatMessage(titleKey)}} + titleText={intl.formatMessage(titleKey)} icon={} headingLevel="h1" /> diff --git a/src/routes/components/state/emptyFilterState/emptyFilterState.tsx b/src/routes/components/state/emptyFilterState/emptyFilterState.tsx index 9f88006dd..7370aec93 100644 --- a/src/routes/components/state/emptyFilterState/emptyFilterState.tsx +++ b/src/routes/components/state/emptyFilterState/emptyFilterState.tsx @@ -114,7 +114,7 @@ const EmptyFilterStateBase: React.FC = ({ > {getItem()} - {intl.formatMessage(title)}} headingLevel="h2" /> + {intl.formatMessage(subTitle)} diff --git a/src/routes/components/state/errorState/errorState.tsx b/src/routes/components/state/errorState/errorState.tsx index 9daa76d01..fc3f60cf1 100644 --- a/src/routes/components/state/errorState/errorState.tsx +++ b/src/routes/components/state/errorState/errorState.tsx @@ -30,7 +30,7 @@ const ErrorStateBase: React.FC = ({ error, icon = ErrorCircleOI return ( - {title}} icon={} headingLevel="h5" /> + } headingLevel="h5" /> {subTitle} ); diff --git a/src/routes/components/state/loadingState/loadingState.tsx b/src/routes/components/state/loadingState/loadingState.tsx index bb3af4d76..0ae19d140 100644 --- a/src/routes/components/state/loadingState/loadingState.tsx +++ b/src/routes/components/state/loadingState/loadingState.tsx @@ -20,7 +20,7 @@ const LoadingStateBase: React.FC = ({ return ( - {heading}} headingLevel="h5" /> + {body} ); diff --git a/src/routes/settings/costModels/components/errorState.tsx b/src/routes/settings/costModels/components/errorState.tsx index ff767b950..3417c2563 100644 --- a/src/routes/settings/costModels/components/errorState.tsx +++ b/src/routes/settings/costModels/components/errorState.tsx @@ -34,7 +34,7 @@ export const ErrorState: React.FC = ({ variant, actionButton, t return ( {title}} + titleText={title} icon={} headingLevel="h4" /> diff --git a/src/routes/settings/costModels/costModel/costModelInfo.tsx b/src/routes/settings/costModels/costModel/costModelInfo.tsx index aa2b07e09..2eb9c3afa 100644 --- a/src/routes/settings/costModels/costModel/costModelInfo.tsx +++ b/src/routes/settings/costModels/costModel/costModelInfo.tsx @@ -93,7 +93,7 @@ class CostModelInfo extends React.Component {intl.formatMessage(messages.costModelsUUIDEmptyState)}} + titleText={intl.formatMessage(messages.costModelsUUIDEmptyState)} icon={} headingLevel="h2" /> diff --git a/src/routes/settings/costModels/costModel/priceListTable.tsx b/src/routes/settings/costModels/costModel/priceListTable.tsx index 82d3e646c..8355ed9a2 100644 --- a/src/routes/settings/costModels/costModel/priceListTable.tsx +++ b/src/routes/settings/costModels/costModel/priceListTable.tsx @@ -293,7 +293,7 @@ class PriceListTable extends React.Component {intl.formatMessage(messages.priceListEmptyRate)}} + titleText={intl.formatMessage(messages.priceListEmptyRate)} icon={} headingLevel="h2" /> diff --git a/src/routes/settings/costModels/costModel/table.tsx b/src/routes/settings/costModels/costModel/table.tsx index ae289c940..efab2048d 100644 --- a/src/routes/settings/costModels/costModel/table.tsx +++ b/src/routes/settings/costModels/costModel/table.tsx @@ -126,7 +126,7 @@ class TableBase extends React.Component {
{intl.formatMessage(messages.costModelsSourceEmptyStateDesc)}} + titleText={intl.formatMessage(messages.costModelsSourceEmptyStateDesc)} icon={} headingLevel="h2" /> diff --git a/src/routes/settings/costModels/costModelWizard/priceListTable.tsx b/src/routes/settings/costModels/costModelWizard/priceListTable.tsx index 2baa4ec7e..cd75a9422 100644 --- a/src/routes/settings/costModels/costModelWizard/priceListTable.tsx +++ b/src/routes/settings/costModels/costModelWizard/priceListTable.tsx @@ -94,7 +94,7 @@ class PriceListTable extends React.Component {intl.formatMessage(messages.costModelsWizardEmptyStateTitle)}} + titleText={intl.formatMessage(messages.costModelsWizardEmptyStateTitle)} icon={} headingLevel="h2" /> diff --git a/src/routes/settings/costModels/costModelWizard/review.tsx b/src/routes/settings/costModels/costModelWizard/review.tsx index 2d064c1ba..338fee686 100644 --- a/src/routes/settings/costModels/costModelWizard/review.tsx +++ b/src/routes/settings/costModels/costModelWizard/review.tsx @@ -36,7 +36,7 @@ const ReviewSuccessBase: React.FC = ({ intl }) => ( {({ onClose, name }) => ( {intl.formatMessage(messages.costModelsWizardReviewStatusTitle)}} + titleText={intl.formatMessage(messages.costModelsWizardReviewStatusTitle)} icon={ diff --git a/src/routes/settings/costModels/costModelsDetails/emptyStateBase.tsx b/src/routes/settings/costModels/costModelsDetails/emptyStateBase.tsx index b0b78d7a2..45f032b9d 100644 --- a/src/routes/settings/costModels/costModelsDetails/emptyStateBase.tsx +++ b/src/routes/settings/costModels/costModelsDetails/emptyStateBase.tsx @@ -11,7 +11,7 @@ interface EmptyStateBaseProps { function EmptyStateBase(props: EmptyStateBaseProps): JSX.Element { return ( - {props.title}} icon={} headingLevel="h2" /> + } headingLevel="h2" /> {props.description} {props.actions ? props.actions : null} diff --git a/src/routes/settings/tagLabels/tagMappings/tagMappings.styles.ts b/src/routes/settings/tagLabels/tagMappings/tagMappings.styles.ts index 690536658..01ecb7545 100644 --- a/src/routes/settings/tagLabels/tagMappings/tagMappings.styles.ts +++ b/src/routes/settings/tagLabels/tagMappings/tagMappings.styles.ts @@ -6,6 +6,9 @@ export const styles = { action: { marginLeft: global_spacer_md.var, }, + emptyStateContainer: { + paddingTop: global_spacer_md.value, + }, pagination: { backgroundColor: global_BackgroundColor_light_100.value, paddingBottom: global_spacer_md.value, diff --git a/src/routes/settings/tagLabels/tagMappings/tagMappings.tsx b/src/routes/settings/tagLabels/tagMappings/tagMappings.tsx index e7fa8a531..13dd6de8b 100644 --- a/src/routes/settings/tagLabels/tagMappings/tagMappings.tsx +++ b/src/routes/settings/tagLabels/tagMappings/tagMappings.tsx @@ -1,3 +1,4 @@ +import Unavailable from '@patternfly/react-component-groups/dist/esm/UnavailableContent'; import { Pagination, PaginationVariant } from '@patternfly/react-core'; import type { Query } from 'api/queries/query'; import { getQuery } from 'api/queries/query'; @@ -10,7 +11,6 @@ import { useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import type { AnyAction } from 'redux'; import type { ThunkDispatch } from 'redux-thunk'; -import { NotAvailable } from 'routes/components/page/notAvailable'; import { LoadingState } from 'routes/components/state/loadingState'; import * as queryUtils from 'routes/utils/query'; import type { RootState } from 'store'; @@ -18,6 +18,7 @@ import { FetchStatus } from 'store/common'; import { settingsActions, settingsSelectors } from 'store/settings'; import { styles } from './tagMappings.styles'; +import { TagMappingsEmptyState } from './tagMappingsEmptyState'; import { TagMappingsTable } from './tagMappingsTable'; import { TagMappingsToolbar } from './tagMappingsToolbar'; @@ -43,7 +44,7 @@ const baseQuery: Query = { offset: 0, filter_by: {}, order_by: { - key: 'asc', + parent: 'asc', }, }; @@ -100,14 +101,14 @@ const TagMappings: React.FC = ({ canWrite }) => { ); }; - const getToolbar = (tags: SettingsData[]) => { + const getToolbar = (mappings: SettingsData[]) => { const itemsTotal = settings?.meta ? settings.meta.count : 0; return ( handleOnFilterAdded(filter)} @@ -148,11 +149,12 @@ const TagMappings: React.FC = ({ canWrite }) => { }; if (settingsError) { - return ; + return ; } - const tags = getMappings(); - const isDisabled = tags.length === 0; + const mappings = getMappings(); + const isDisabled = mappings.length === 0; + const hasMappings = mappings.length > 0 && !Object.keys(query.filter_by).length; // no filter applied return ( <> @@ -166,14 +168,18 @@ const TagMappings: React.FC = ({ canWrite }) => { warning: {intl.formatMessage(messages.tagMappingWarning)}, })}
- {getToolbar(tags)} + {hasMappings && getToolbar(mappings)} {settingsStatus === FetchStatus.inProgress ? ( - ) : ( + ) : hasMappings ? ( <> {getTable()}
{getPagination(isDisabled, true)}
+ ) : ( +
+ +
)} ); @@ -191,32 +197,20 @@ const useMapToProps = ({ query }: MappingsMapProps): MappingsStateProps => { }; const settingsQueryString = getQuery(settingsQuery); const settings = useSelector((state: RootState) => - settingsSelectors.selectSettings(state, SettingsType.tags, settingsQueryString) + settingsSelectors.selectSettings(state, SettingsType.tagsMappings, settingsQueryString) ); const settingsStatus = useSelector((state: RootState) => - settingsSelectors.selectSettingsStatus(state, SettingsType.tags, settingsQueryString) + settingsSelectors.selectSettingsStatus(state, SettingsType.tagsMappings, settingsQueryString) ); const settingsError = useSelector((state: RootState) => - settingsSelectors.selectSettingsError(state, SettingsType.tags, settingsQueryString) - ); - - const settingsUpdateDisableStatus = useSelector((state: RootState) => - settingsSelectors.selectSettingsUpdateStatus(state, SettingsType.tagsDisable) - ); - const settingsUpdateEnableStatus = useSelector((state: RootState) => - settingsSelectors.selectSettingsUpdateStatus(state, SettingsType.tagsEnable) + settingsSelectors.selectSettingsError(state, SettingsType.tagsMappings, settingsQueryString) ); useEffect(() => { - if ( - !settingsError && - settingsStatus !== FetchStatus.inProgress && - settingsUpdateDisableStatus !== FetchStatus.inProgress && - settingsUpdateEnableStatus !== FetchStatus.inProgress - ) { - dispatch(settingsActions.fetchSettings(SettingsType.tags, settingsQueryString)); + if (!settingsError && settingsStatus !== FetchStatus.inProgress) { + dispatch(settingsActions.fetchSettings(SettingsType.tagsMappings, settingsQueryString)); } - }, [query, settingsUpdateDisableStatus, settingsUpdateEnableStatus]); + }, [query]); return { settings, diff --git a/src/routes/settings/tagLabels/tagMappings/tagMappingsEmptyState.tsx b/src/routes/settings/tagLabels/tagMappings/tagMappingsEmptyState.tsx new file mode 100644 index 000000000..61fd1038b --- /dev/null +++ b/src/routes/settings/tagLabels/tagMappings/tagMappingsEmptyState.tsx @@ -0,0 +1,73 @@ +import { + Button, + ButtonVariant, + EmptyState, + EmptyStateActions, + EmptyStateBody, + EmptyStateFooter, + EmptyStateHeader, + EmptyStateIcon, + EmptyStateVariant, + Tooltip, +} from '@patternfly/react-core'; +import { PlusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon'; +import messages from 'locales/messages'; +import React from 'react'; +import { useIntl } from 'react-intl'; + +interface TagMappingsEmptyStateOwnProps { + canWrite?: boolean; + isDisabled?: boolean; + onCreateTagMapping(); +} + +type TagMappingsEmptyStateProps = TagMappingsEmptyStateOwnProps; + +const TagMappingsEmptyState: React.FC = ({ + canWrite, + isDisabled, + onCreateTagMapping, +}: TagMappingsEmptyStateOwnProps) => { + const intl = useIntl(); + + const getActions = () => { + const getTooltip = children => { + if (!canWrite) { + const disableTagsTooltip = intl.formatMessage(messages.readOnlyPermissions); + return {children}; + } + return children; + }; + + return getTooltip( + + ); + }; + + return ( + + } + headingLevel="h5" + /> + + {intl.formatMessage(messages.noMappedTagsDesc, { + learnMore: ( + + {intl.formatMessage(messages.learnMore)} + + ), + warning: {intl.formatMessage(messages.noMappedTagsWarning)}, + })} + + + {getActions()} + + + ); +}; + +export { TagMappingsEmptyState }; diff --git a/src/routes/settings/tagLabels/tags/tags.tsx b/src/routes/settings/tagLabels/tags/tags.tsx index 207f99736..2c5edeca2 100644 --- a/src/routes/settings/tagLabels/tags/tags.tsx +++ b/src/routes/settings/tagLabels/tags/tags.tsx @@ -1,3 +1,4 @@ +import Unavailable from '@patternfly/react-component-groups/dist/esm/UnavailableContent'; import { Pagination, PaginationVariant } from '@patternfly/react-core'; import type { Query } from 'api/queries/query'; import { getQuery } from 'api/queries/query'; @@ -10,7 +11,6 @@ import { useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import type { AnyAction } from 'redux'; import type { ThunkDispatch } from 'redux-thunk'; -import { NotAvailable } from 'routes/components/page/notAvailable'; import { LoadingState } from 'routes/components/state/loadingState'; import * as queryUtils from 'routes/utils/query'; import type { RootState } from 'store'; @@ -213,7 +213,7 @@ const Tags: React.FC = ({ canWrite }) => { }; if (settingsError) { - return ; + return ; } const tags = getTags();