From 83bf3184fb6ddfe0a0a09ecb616bb20ec4d50c07 Mon Sep 17 00:00:00 2001 From: Ruben van Leeuwen Date: Mon, 9 Oct 2023 11:46:22 +0200 Subject: [PATCH] 278-action-buttons: Add tooltip with the reason an action is disabled --- .../WFOSubscriptionActions.tsx | 149 ++++++++++++++---- .../WFOSubscription/utils/utils.spec.ts | 32 ++++ .../WFOSubscription/utils/utils.tsx | 16 ++ .../src/hooks/useSubscriptionActions.ts | 6 +- .../src/messages/en-US.json | 14 +- .../src/messages/nl-NL.json | 14 +- 6 files changed, 192 insertions(+), 39 deletions(-) diff --git a/packages/orchestrator-ui-components/src/components/WFOSubscription/WFOSubscriptionActions.tsx b/packages/orchestrator-ui-components/src/components/WFOSubscription/WFOSubscriptionActions.tsx index 9561e0eeb..d7d954d53 100644 --- a/packages/orchestrator-ui-components/src/components/WFOSubscription/WFOSubscriptionActions.tsx +++ b/packages/orchestrator-ui-components/src/components/WFOSubscription/WFOSubscriptionActions.tsx @@ -9,12 +9,20 @@ import { EuiAvatar, EuiTitle, EuiPopover, + EuiToolTip, } from '@elastic/eui'; import { useTranslations } from 'next-intl'; + +import { useOrchestratorTheme } from '../../hooks'; + +import { TranslationValues } from 'next-intl'; + import { SubscriptionAction, useSubscriptionActions, } from '../../hooks/useSubscriptionActions'; +import { WFOXCircleFill } from '../../icons'; +import { ReactJSXElement } from '@emotion/react/types/jsx-namespace'; type MenuItemProps = { key: string; @@ -39,25 +47,9 @@ export type WFOSubscriptionActionsProps = { export const WFOSubscriptionActions: FC = ({ subscriptionId, }) => { - const MenuItem: FC = ({ icon, action, key }) => { - return ( - - } - > - {action.description} - - - ); - }; + const { theme } = useOrchestratorTheme(); - const t = useTranslations('subscriptions.detail.workflow'); + const t = useTranslations('subscriptions.detail.actions'); const [isPopoverOpen, setPopover] = useState(false); const { data: subscriptionActions } = useSubscriptionActions(subscriptionId); @@ -70,6 +62,113 @@ export const WFOSubscriptionActions: FC = ({ setPopover(false); }; + const MenuItem: FC = ({ icon, action }) => { + // Change icon to include x if there's a reason + // Add tooltip with reason + const linkIt = (actionItem: ReactJSXElement) => { + return ( + + {actionItem} + + ); + }; + + const flattenArrayProps = (): TranslationValues => { + const flatObject: TranslationValues = {}; + for (const [key, value] of Object.entries(action)) { + if (Array.isArray(value)) { + flatObject[key] = value.join(', '); + } else { + flatObject[key] = value; + } + } + return action ? flatObject : {}; + }; + + const tooltipIt = (actionItem: ReactJSXElement) => { + /** + Whether an action is disabled is indicated by it having a reason property. + The value of the reason property is as a translation key that should + be part of the local translations under subscription.details.workflow.disableReasons + Some of these reasons may contain dynamic values. The values are passed as extra keys next to + the reason key. The complete reason object is passed to the translate function to make this work. + An extra variable passed in might be of type array, before passing it in arrays are flattened to , + concatenated strings. + + Example action item response for an action that is disabled + const reason = { + name: "...", + description: "...", + reason: "random_reason_translation_key" => + this maps to a key in subscription.details.workflow.disableReasons containing + ".... {randomVar1} .... {randomVar2} " + randomVar: [ + "array value 1", + "array value 2" + ], + randomVar2: "flat string" + + } + + // Translation function invocation + t('randonReason', reason) + */ + if (!action.reason) return actionItem; + + const tooltipContent = t(action.reason, flattenArrayProps()); + + return ( +
+ + {actionItem} + +
+ ); + }; + + const getIcon = () => { + return action.reason ? ( +
+ +
+ +
+
+ ) : ( +
+ +
+ ); + }; + + const ActionItem = () => ( + + {action.description} + + ); + + return action?.reason + ? tooltipIt() + : linkIt(); + }; + const button = ( = ({ > - {subscriptionActions && subscriptionActions.create && ( - <> - - {subscriptionActions.create.map((action, index) => ( - - ))} - - )} - {subscriptionActions && subscriptionActions.modify && ( <> diff --git a/packages/orchestrator-ui-components/src/components/WFOSubscription/utils/utils.spec.ts b/packages/orchestrator-ui-components/src/components/WFOSubscription/utils/utils.spec.ts index f75888faf..a49b63280 100644 --- a/packages/orchestrator-ui-components/src/components/WFOSubscription/utils/utils.spec.ts +++ b/packages/orchestrator-ui-components/src/components/WFOSubscription/utils/utils.spec.ts @@ -1,7 +1,9 @@ +import { SubscriptionAction } from '../../../hooks'; import { FieldValue } from '../../../types'; import { getFieldFromProductBlockInstanceValues, getProductBlockTitle, + flattenArrayProps, } from './utils'; describe('getFieldFromProductBlockInstanceValues()', () => { @@ -68,3 +70,33 @@ describe('getProductBlockTitle()', () => { expect(getProductBlockTitle(instanceValues)).toBe(''); }); }); + +describe('flattenArrayProps', () => { + it('should flatten an object with array values into a comma-separated string', () => { + const action: SubscriptionAction = { + name: 'action name', + description: 'action description', + usable_when: ['Status1', 'Status2', 'Status3'], + }; + + const result = flattenArrayProps(action); + + expect(result).toEqual({ + name: 'action name', + description: 'action description', + usable_when: 'Status1, Status2, Status3', + }); + }); + + it('should handle an object with non-array values', () => { + const action: SubscriptionAction = { + name: 'action name', + description: 'action description', + }; + const result = flattenArrayProps(action); + expect(result).toEqual({ + name: 'action name', + description: 'action description', + }); + }); +}); diff --git a/packages/orchestrator-ui-components/src/components/WFOSubscription/utils/utils.tsx b/packages/orchestrator-ui-components/src/components/WFOSubscription/utils/utils.tsx index 12eca2ad5..35d4926e2 100644 --- a/packages/orchestrator-ui-components/src/components/WFOSubscription/utils/utils.tsx +++ b/packages/orchestrator-ui-components/src/components/WFOSubscription/utils/utils.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { EuiIcon } from '@elastic/eui'; +import { TranslationValues } from 'next-intl'; +import { SubscriptionAction } from '../../../hooks'; import { FieldValue } from '../../../types'; const MAX_LABEL_LENGTH = 45; @@ -63,3 +65,17 @@ export const getProductBlockTitle = ( ? `${title.substring(0, MAX_LABEL_LENGTH)}...` : title; }; + +export const flattenArrayProps = ( + action: SubscriptionAction, +): TranslationValues => { + const flatObject: TranslationValues = {}; + for (const [key, value] of Object.entries(action)) { + if (Array.isArray(value)) { + flatObject[key] = value.join(', '); + } else { + flatObject[key] = value; + } + } + return action ? flatObject : {}; +}; diff --git a/packages/orchestrator-ui-components/src/hooks/useSubscriptionActions.ts b/packages/orchestrator-ui-components/src/hooks/useSubscriptionActions.ts index eb522ba0b..64cdfcfd4 100644 --- a/packages/orchestrator-ui-components/src/hooks/useSubscriptionActions.ts +++ b/packages/orchestrator-ui-components/src/hooks/useSubscriptionActions.ts @@ -17,7 +17,6 @@ export interface SubscriptionAction { interface SubscriptionActions { reason?: string; locked_relations?: string[]; - create: SubscriptionAction[]; modify: SubscriptionAction[]; terminate: SubscriptionAction[]; system: SubscriptionAction[]; @@ -27,7 +26,7 @@ export const useSubscriptionActions = (subscriptionId: string) => { const { subscriptionActionsEndpoint } = useContext( OrchestratorConfigContext, ); - //https://orchestrator.dev.automation.surf.net/api/subscriptions/workflows/77466b50-951f-4362-a817-96ee66e63574 + const fetchSubscriptionActions = async () => { const response = await fetch( `${subscriptionActionsEndpoint}/${subscriptionId}`, @@ -35,7 +34,8 @@ export const useSubscriptionActions = (subscriptionId: string) => { method: 'GET', }, ); - return (await response.json()) as SubscriptionActions; + const actions = (await response.json()) as SubscriptionActions; + return actions; }; return useQuery('subscriptionActions', fetchSubscriptionActions); diff --git a/packages/orchestrator-ui-components/src/messages/en-US.json b/packages/orchestrator-ui-components/src/messages/en-US.json index ff911d2f7..1708db73f 100644 --- a/packages/orchestrator-ui-components/src/messages/en-US.json +++ b/packages/orchestrator-ui-components/src/messages/en-US.json @@ -138,12 +138,22 @@ "relatedSubscriptions": "Related subscriptions" }, "loadingStatus": "Loading status", - "workflow": { + "actions": { "create": "Create workflow", "modify": "Modify workflow", "system": "System workflow", "terminate": "Terminate workflow", - "actions": "Actions" + "actions": "Actions", + "subscription": { + "no_modify_deleted_related_objects": "This subscription can not be modified because it contains references to other systems that are deleted.", + "no_modify_in_use_by_subscription": "This subscription can not be {action} as it is used in other subscriptions: {unterminated_in_use_by_subscriptions}", + "no_modify_invalid_status": "This subscription can not be modified because of the status: {status}. Only subscriptions with status {usable_when} can be {action}.", + "no_modify_workflow": "This subscription can not be modified as the product has no modify workflows.", + "no_termination_workflow": "This subscription can not be terminated as the product has no termination workflows.", + "no_validate_workflow": "This subscription can not be validated as the product has no validate workflows.", + "not_in_sync": "This subscription can not be modified because it is not in sync. This means there is some error in the registration of the subscription or that it is being modified by another workflow.", + "relations_not_in_sync": "This subscription can not be modified because some related subscriptions are not insync. Locked subscriptions: {locked_relations}" + } }, "subscriptionDetails": "Subscription details", "fixedInputs": "Fixed inputs", diff --git a/packages/orchestrator-ui-components/src/messages/nl-NL.json b/packages/orchestrator-ui-components/src/messages/nl-NL.json index 2886560a0..1c7749696 100644 --- a/packages/orchestrator-ui-components/src/messages/nl-NL.json +++ b/packages/orchestrator-ui-components/src/messages/nl-NL.json @@ -138,12 +138,22 @@ "relatedSubscriptions": "Geralateerde subscripties" }, "loadingStatus": "Laden", - "workflow": { + "actions": { "create": "Create workflow", "modify": "Modify workflow", "system": "System workflow", "terminate": "Terminate workflow", - "actions": "Acties" + "actions": "Acties", + "subscription": { + "no_modify_deleted_related_objects": "This subscription can not be modified because it contains references to other systems that are deleted.", + "no_modify_in_use_by_subscription": "This subscription can not be {action} as it is used in other subscriptions: {unterminated_in_use_by_subscriptions}", + "no_modify_invalid_status": "This subscription can not be modified because of the status: {status}. Only subscriptions with status {usable_when} can be {action}.", + "no_modify_workflow": "This subscription can not be modified as the product has no modify workflows.", + "no_termination_workflow": "This subscription can not be terminated as the product has no termination workflows.", + "no_validate_workflow": "This subscription can not be validated as the product has no validate workflows.", + "not_in_sync": "This subscription can not be modified because it is not in sync. This means there is some error in the registration of the subscription or that it is being modified by another workflow.", + "relations_not_in_sync": "This subscription can not be modified because some related subscriptions are not insync. Locked subscriptions: {locked_relations}" + } }, "subscriptionDetails": "Subscription detail", "fixedInputs": "Fixed inputs",