From 907bcfcaf99c9eb99e6f9245ac6a5f49505c753d Mon Sep 17 00:00:00 2001 From: Fredrik Hatletvedt Date: Wed, 6 Sep 2023 09:57:44 +0200 Subject: [PATCH 1/3] improve mapRouteParamsToProps typing --- src/utils/routing.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/utils/routing.ts b/src/utils/routing.ts index 89233ad0a..e65273233 100644 --- a/src/utils/routing.ts +++ b/src/utils/routing.ts @@ -10,6 +10,10 @@ import { routeWithParams } from './string'; import { routes } from '../routes'; +/** Mark specific keys of an object optional */ +type Optionalize = Omit & + Partial>; + /** * Maps route parameters as defined in react-router and injects them as * first-class props into a component. This avoids components being tightly @@ -38,10 +42,10 @@ export function mapRouteParamsToProps< P extends {}, M extends (keyof P)[] | { [K in keyof P]?: string }, >( - propMap: M, + propMap: M extends (infer K)[] ? [K?, ...K[]] : M, Component: FunctionComponent

| ComponentClass

): ( - props: Pick> + props: Optionalize : keyof M> ) => ReactElement

{ return function (props) { const params = useParams

(); From 6dcafb81348f1e473d157208d4e16dfda2e6c11a Mon Sep 17 00:00:00 2001 From: Fredrik Hatletvedt Date: Wed, 6 Sep 2023 10:03:22 +0200 Subject: [PATCH 2/3] allow loading and error content to be suppressed --- src/components/app-list-item/index.tsx | 11 +- src/components/app-list/index.tsx | 6 +- src/components/async-resource/index.tsx | 157 ++++++++++-------- .../async-resource/simple-async-resource.tsx | 105 ++++++------ .../scheduled-job/scheduled-batch-list.tsx | 2 +- .../secrets/active-component-secrets.tsx | 51 ++++-- ...t-list-item-title-azure-key-vault-item.tsx | 1 + .../environments-summary/environment-card.tsx | 6 +- src/components/job-overview/step-summary.tsx | 2 +- .../component-replica-log-accordion.tsx | 2 +- .../job-component-vulnerability-details.tsx | 2 +- .../page-environment/component-list.tsx | 33 ++-- src/components/replica/index.tsx | 9 +- src/pages/page-application/index.tsx | 2 +- 14 files changed, 216 insertions(+), 173 deletions(-) diff --git a/src/components/app-list-item/index.tsx b/src/components/app-list-item/index.tsx index 944e1a474..3916c1a27 100644 --- a/src/components/app-list-item/index.tsx +++ b/src/components/app-list-item/index.tsx @@ -13,7 +13,7 @@ import { Link } from 'react-router-dom'; import { useGetVulnerabilities } from './use-get-vulnerabilities'; -import AsyncResource from '../async-resource/simple-async-resource'; +import { SimpleAsyncResource } from '../async-resource/simple-async-resource'; import { AppBadge } from '../app-badge'; import { EnvironmentCardStatus, @@ -83,7 +83,6 @@ const AppItemStatus: FunctionComponent = ({ name, }) => { const [state] = useGetVulnerabilities(name); - const vulnerabilities = (state.data ?? []).reduce( (obj, x) => aggregateVulnerabilitySummaries([ @@ -119,10 +118,10 @@ const AppItemStatus: FunctionComponent = ({

- } - customError={<>} + loadingContent={false} + errorContent={false} > {visibleKeys.some((key) => vulnerabilities[key] > 0) && ( = ({ visibleKeys={visibleKeys} /> )} - + {(environmentActiveComponents || latestJob) && ( = ({
} + loadingContent={} > {favouriteApps.length > 0 ? (
@@ -183,7 +183,9 @@ export const AppList: FunctionComponent = ({ } + loadingContent={ + + } > {apps.length > 0 && (
diff --git a/src/components/async-resource/index.tsx b/src/components/async-resource/index.tsx index 70268bed3..cd565136c 100644 --- a/src/components/async-resource/index.tsx +++ b/src/components/async-resource/index.tsx @@ -19,11 +19,12 @@ import { hasData, isLoading, } from '../../state/subscriptions'; +import { isNullOrUndefined } from '../../utils/object'; interface AsyncResourcePropsBase extends SubscriptionObjectState { - failedContent?: ReactNode; - loading?: ReactNode; + loadingContent?: ReactNode; + errorContent?: ReactNode; resource: R; resourceParams: P; } @@ -34,6 +35,17 @@ export interface AsyncResourceProps export interface AsyncResourceStrictProps extends AsyncResourcePropsBase> {} +const LoadingComponent: FunctionComponent<{ + content?: ReactNode; + defaultContent: React.JSX.Element; +}> = ({ content, defaultContent }) => + // if content is a boolean the intent is either to display or hide the default content + !isNullOrUndefined(content) && content !== true ? ( + <>{content !== false && content} + ) : ( + defaultContent + ); + export const AsyncResource: FunctionComponent< PropsWithChildren > = ({ @@ -41,75 +53,76 @@ export const AsyncResource: FunctionComponent< hasData, isLoading, error, - failedContent, - loading, + loadingContent = true, + errorContent = true, resource, resourceParams, -}) => { - if (!hasData && isLoading) { - return loading ? ( - <>{loading} - ) : ( - - Loading… - - ); - } else if (error) { - return failedContent ? ( - <>{failedContent} - ) : ( - - - That didn't work{' '} - - 😞 - - - - Error subscribing to resource {resource} - {resourceParams?.length > 0 && ( - <> - {' '} - with parameter{resourceParams.length > 1 ? 's' : ''}{' '} - {resourceParams.map((param, idx) => ( - - {param} - {idx < resourceParams.length - 1 ? ', ' : ''} - - ))} - - )} - -
- Error message: - {error} -
- - You may want to refresh the page. If the problem persists, get in - touch on our Slack{' '} - - support channel +}) => + !hasData && isLoading ? ( + + Loading… + + } + /> + ) : error ? ( + + + That didn't work{' '} + + 😞 + - -
- ); - } else { - return <>{children}; - } -}; + + Error subscribing to resource {resource} + {resourceParams?.length > 0 && ( + <> + {' '} + with parameter{resourceParams.length > 1 ? 's' : ''}{' '} + {resourceParams.map((param, idx) => ( + + {param} + {idx < resourceParams.length - 1 ? ', ' : ''} + + ))} + + )} + +
+ Error message: + {error} +
+ + You may want to refresh the page. If the problem persists, get in + touch on our Slack{' '} + + support channel + + + + } + /> + ) : ( + <>{children} + ); AsyncResource.propTypes = { children: PropTypes.node, hasData: PropTypes.bool.isRequired, isLoading: PropTypes.bool.isRequired, error: PropTypes.string, - failedContent: PropTypes.node, - loading: PropTypes.node, + loadingContent: PropTypes.node, + errorContent: PropTypes.node, resource: PropTypes.string.isRequired, resourceParams: PropTypes.arrayOf(PropTypes.string).isRequired, }; @@ -118,16 +131,16 @@ export const AsyncResourceConnected: FunctionComponent< PropsWithChildren> > = (props) => { const [AsyncResourceConnected] = useState(() => - connect( - ( - state: RootState, - { resource, resourceParams }: AsyncResourceStrictProps - ) => ({ - error: getError(state, resource, resourceParams), - hasData: hasData(state, resource, resourceParams), - isLoading: isLoading(state, resource, resourceParams), - }) - )(AsyncResource) + connect< + SubscriptionObjectState, + object, + AsyncResourceStrictProps, + RootState + >((state, { resource, resourceParams }) => ({ + error: getError(state, resource, resourceParams), + hasData: hasData(state, resource, resourceParams), + isLoading: isLoading(state, resource, resourceParams), + }))(AsyncResource) ); return ( diff --git a/src/components/async-resource/simple-async-resource.tsx b/src/components/async-resource/simple-async-resource.tsx index d53897646..588706baa 100644 --- a/src/components/async-resource/simple-async-resource.tsx +++ b/src/components/async-resource/simple-async-resource.tsx @@ -1,67 +1,78 @@ import { CircularProgress, Typography } from '@equinor/eds-core-react'; -import React, { PropsWithChildren, ReactNode } from 'react'; +import React, { FunctionComponent, PropsWithChildren, ReactNode } from 'react'; import { Alert } from '../alert'; import { AsyncState } from '../../effects/effect-types'; import { externalUrls } from '../../externalUrls'; import { RequestState } from '../../state/state-utils/request-states'; +import { isNullOrUndefined } from '../../utils/object'; export interface SimpleAsyncResourceProps { asyncState: AsyncState; - loading?: React.JSX.Element; - customError?: ReactNode; + loadingContent?: ReactNode; + errorContent?: ReactNode; } +const LoadingComponent: FunctionComponent<{ + content?: ReactNode; + defaultContent: React.JSX.Element; +}> = ({ content, defaultContent }) => + // if content is a boolean the intent is either to display or hide the default content + !isNullOrUndefined(content) && content !== true ? ( + <>{content !== false && content} + ) : ( + defaultContent + ); + export const SimpleAsyncResource = ({ asyncState, children, - loading, - customError, -}: PropsWithChildren>): React.JSX.Element => { - if (!asyncState || asyncState.status === RequestState.IN_PROGRESS) { - return ( - loading || ( + loadingContent = true, + errorContent = true, +}: PropsWithChildren>): React.JSX.Element => + !asyncState || asyncState.status === RequestState.IN_PROGRESS ? ( + Loading… - ) - ); - } - - if (asyncState.error) { - return customError ? ( - <>{customError} - ) : ( - - - That didn't work{' '} - - 😞 - - -
-
- Error message: - {asyncState.error} -
- - You may want to refresh the page. If the problem persists, get in - touch on our Slack{' '} - - support channel - + } + /> + ) : asyncState.error ? ( + + + That didn't work{' '} + + 😞 + -
-
- ); - } - - return <>{children}; -}; +
+
+ Error message: + {asyncState.error} +
+ + You may want to refresh the page. If the problem persists, get in + touch on our Slack{' '} + + support channel + + +
+ + } + /> + ) : ( + <>{children} + ); export default SimpleAsyncResource; diff --git a/src/components/component/scheduled-job/scheduled-batch-list.tsx b/src/components/component/scheduled-job/scheduled-batch-list.tsx index 69587ea3e..907b1ab97 100644 --- a/src/components/component/scheduled-job/scheduled-batch-list.tsx +++ b/src/components/component/scheduled-job/scheduled-batch-list.tsx @@ -133,7 +133,7 @@ export const ScheduledBatchList: FunctionComponent = ({ - Batches ({sortedData.length ?? '...'}) + Batches ({sortedData.length ?? '…'}) diff --git a/src/components/component/secrets/active-component-secrets.tsx b/src/components/component/secrets/active-component-secrets.tsx index e00418889..f4bbd12e5 100644 --- a/src/components/component/secrets/active-component-secrets.tsx +++ b/src/components/component/secrets/active-component-secrets.tsx @@ -15,6 +15,8 @@ import { getComponentSecret, getMemoizedEnvironment, } from '../../../state/environment'; +import { sortCompareString } from '../../../utils/sort-utils'; +import { SecretType } from '../../../models/radix-api/secrets/secret-type'; interface ActiveComponentSecretsData { environment?: EnvironmentModel; @@ -28,23 +30,40 @@ export interface ActiveComponentSecretsProps secretNames?: Array; } -function buildSecrets( - secretNames: Array, - componentName: string, - environment?: EnvironmentModel -): Array<{ name: string; secret: SecretModel }> { - return secretNames.map((secretName) => ({ - name: secretName, - secret: getComponentSecret(environment, secretName, componentName), - })); -} - export const ActiveComponentSecrets: FunctionComponent< ActiveComponentSecretsProps > = ({ appName, envName, componentName, secretNames, environment }) => { - const [secrets, setSecrets] = useState([]); + const [secrets, setSecrets] = useState>([]); useEffect(() => { - setSecrets(buildSecrets(secretNames, componentName, environment)); + const componentSecrets = (secretNames || []).map((secretName) => + getComponentSecret(environment, secretName, componentName) + ); + + const sortedData = componentSecrets.sort((x, y) => + sortCompareString(x.name, y.name) + ); + + type GroupedSecretMap = Record>; + const groupedSecrets = sortedData.reduce( + (obj, secret) => { + const key = secret.type || SecretType.SecretTypeGeneric; + return { ...obj, ...{ [key]: [...obj[key], secret] } }; + }, + Object.values(SecretType) + .sort((x, y) => sortCompareString(x, y)) + .reduce((obj, key) => ({ ...obj, [key]: [] }), {} as GroupedSecretMap) + ); + + const minimized = Object.keys(groupedSecrets).reduce>( + (obj, key) => [...obj, ...groupedSecrets[key]], + [] + ); + + console.log('sortedData', sortedData); + console.log('groupedSecrets', groupedSecrets); + console.log('moshpit', minimized); + + setSecrets(minimized); }, [secretNames, componentName, environment]); return ( @@ -53,16 +72,16 @@ export const ActiveComponentSecrets: FunctionComponent< - Secrets ({secrets?.length ?? '...'}) + Secrets ({secrets.length ?? '…'})
{secretNames.length > 0 ? ( - secrets.map(({ name, secret }) => ( + secrets.map((secret) => ( + {/* Splat! No sorting, just place and render... */} {filteredData.map((x, i) => ( ))} diff --git a/src/components/environments-summary/environment-card.tsx b/src/components/environments-summary/environment-card.tsx index d770b5844..cb4edb7e7 100644 --- a/src/components/environments-summary/environment-card.tsx +++ b/src/components/environments-summary/environment-card.tsx @@ -101,7 +101,7 @@ function CardContentBuilder( }; const statusElement = ( - }> + {components.length > 0 && (
{visibleKeys.some((key) => vulnerabilities[key] > 0) && ( @@ -126,13 +126,13 @@ function CardContentBuilder(
), body: ( - }> + ), diff --git a/src/components/job-overview/step-summary.tsx b/src/components/job-overview/step-summary.tsx index 29b421370..8e3d18e81 100644 --- a/src/components/job-overview/step-summary.tsx +++ b/src/components/job-overview/step-summary.tsx @@ -19,7 +19,7 @@ function getComponents(name: string, components: Array): string { if (components?.length > 1) { const maxEnumeratedComponents = 3; return components.length > maxEnumeratedComponents - ? components.slice(0, maxEnumeratedComponents - 1).join(',') + '...' + ? components.slice(0, maxEnumeratedComponents - 1).join(',') + '…' : components.slice(0, -1).join(',') + ' and ' + components.slice(-1); } diff --git a/src/components/page-active-component/component-replica-log-accordion.tsx b/src/components/page-active-component/component-replica-log-accordion.tsx index 06658bc14..ebf70a2a5 100644 --- a/src/components/page-active-component/component-replica-log-accordion.tsx +++ b/src/components/page-active-component/component-replica-log-accordion.tsx @@ -113,7 +113,7 @@ export const ComponentReplicaLogAccordion: FunctionComponent< {title} ( {componentInventory.status === RequestState.IN_PROGRESS - ? '...' + ? '…' : replicas.length} ) diff --git a/src/components/page-active-job-component/job-component-vulnerability-details.tsx b/src/components/page-active-job-component/job-component-vulnerability-details.tsx index 68c8bc0fd..140b6f809 100644 --- a/src/components/page-active-job-component/job-component-vulnerability-details.tsx +++ b/src/components/page-active-job-component/job-component-vulnerability-details.tsx @@ -35,7 +35,7 @@ export const JobComponentVulnerabilityDetails: FunctionComponent< Vulnerabilities ( {state.status === RequestState.IN_PROGRESS - ? '...' + ? '…' : vulnerabilityCount} ) diff --git a/src/components/page-environment/component-list.tsx b/src/components/page-environment/component-list.tsx index 71d892910..94708e99f 100644 --- a/src/components/page-environment/component-list.tsx +++ b/src/components/page-environment/component-list.tsx @@ -36,18 +36,18 @@ import './style.css'; export interface ComponentListProps { appName: string; - environment?: EnvironmentModel; + environment: EnvironmentModel; components: Array; } function getComponentUrl( appName: string, - environment: EnvironmentModel, - component: ComponentModel + envName: string, + { name, type }: ComponentModel ): string { - return component.type === ComponentType.job - ? getActiveJobComponentUrl(appName, environment.name, component.name) - : getActiveComponentUrl(appName, environment.name, component.name); + return type === ComponentType.job + ? getActiveJobComponentUrl(appName, envName, name) + : getActiveComponentUrl(appName, envName, name); } function getEnvironmentComponentScanModel( @@ -55,7 +55,7 @@ function getEnvironmentComponentScanModel( name: string, type: ComponentType ): ImageWithLastScanModel { - let componentKey = ''; + let componentKey = '' as keyof EnvironmentVulnerabilitiesModel; switch (type) { case ComponentType.component: componentKey = 'components'; @@ -114,19 +114,15 @@ const EnvironmentComponentScanSummary: FunctionComponent<{ export const ComponentList: FunctionComponent = ({ appName, - environment, + environment: { name: envName }, components, }) => { + const [environmentVulnerabilities] = useGetEnvironmentScans(appName, envName); const [compMap, setCompMap] = useState>>( {} ); useEffect(() => setCompMap(buildComponentMap(components)), [components]); - const [environmentVulnerabilities] = useGetEnvironmentScans( - appName, - environment.name - ); - return ( <> {Object.keys(compMap).map((type) => ( @@ -162,7 +158,7 @@ export const ComponentList: FunctionComponent = ({ {compMap[type].map((x, i) => ( - + {x.name} @@ -174,7 +170,7 @@ export const ComponentList: FunctionComponent = ({ @@ -182,7 +178,7 @@ export const ComponentList: FunctionComponent = ({ {environmentVulnerabilities.error} } > @@ -210,9 +206,8 @@ export const ComponentList: FunctionComponent = ({ ComponentList.propTypes = { appName: PropTypes.string.isRequired, - environment: PropTypes.shape( - EnvironmentModelValidationMap - ) as PropTypes.Validator, + environment: PropTypes.shape(EnvironmentModelValidationMap) + .isRequired as PropTypes.Validator, components: PropTypes.arrayOf( PropTypes.shape( ComponentModelValidationMap diff --git a/src/components/replica/index.tsx b/src/components/replica/index.tsx index 2b24f7b71..f83931344 100644 --- a/src/components/replica/index.tsx +++ b/src/components/replica/index.tsx @@ -2,7 +2,7 @@ import { Accordion, Typography } from '@equinor/eds-core-react'; import * as PropTypes from 'prop-types'; import React, { FunctionComponent, useState } from 'react'; -import AsyncResource from '../async-resource/simple-async-resource'; +import { SimpleAsyncResource } from '../async-resource/simple-async-resource'; import { Code } from '../code'; import { Log, @@ -146,7 +146,10 @@ export const Replica: FunctionComponent = ({ )}
- + {replica && logState?.data ? ( isCollapsibleLog ? ( @@ -177,7 +180,7 @@ export const Replica: FunctionComponent = ({ ) : ( This replica has no log )} - +
); diff --git a/src/pages/page-application/index.tsx b/src/pages/page-application/index.tsx index 8e175867c..6d423f45f 100644 --- a/src/pages/page-application/index.tsx +++ b/src/pages/page-application/index.tsx @@ -54,7 +54,7 @@ export const PageApplication: FunctionComponent = ({ } + loadingContent={false} > {!application?.userIsAdmin && ( From 1efdd18206f98d212951da3f7527082ca24c7f6d Mon Sep 17 00:00:00 2001 From: Fredrik Hatletvedt Date: Wed, 6 Sep 2023 10:45:37 +0200 Subject: [PATCH 3/3] disable error message for header component --- src/components/async-resource/index.tsx | 2 +- .../secrets/active-component-secrets.tsx | 28 +------------------ ...t-list-item-title-azure-key-vault-item.tsx | 1 - src/pages/page-application/index.tsx | 7 +++-- 4 files changed, 7 insertions(+), 31 deletions(-) diff --git a/src/components/async-resource/index.tsx b/src/components/async-resource/index.tsx index cd565136c..6be2a72a1 100644 --- a/src/components/async-resource/index.tsx +++ b/src/components/async-resource/index.tsx @@ -133,7 +133,7 @@ export const AsyncResourceConnected: FunctionComponent< const [AsyncResourceConnected] = useState(() => connect< SubscriptionObjectState, - object, + {}, AsyncResourceStrictProps, RootState >((state, { resource, resourceParams }) => ({ diff --git a/src/components/component/secrets/active-component-secrets.tsx b/src/components/component/secrets/active-component-secrets.tsx index f4bbd12e5..65741c8b6 100644 --- a/src/components/component/secrets/active-component-secrets.tsx +++ b/src/components/component/secrets/active-component-secrets.tsx @@ -15,8 +15,6 @@ import { getComponentSecret, getMemoizedEnvironment, } from '../../../state/environment'; -import { sortCompareString } from '../../../utils/sort-utils'; -import { SecretType } from '../../../models/radix-api/secrets/secret-type'; interface ActiveComponentSecretsData { environment?: EnvironmentModel; @@ -39,31 +37,7 @@ export const ActiveComponentSecrets: FunctionComponent< getComponentSecret(environment, secretName, componentName) ); - const sortedData = componentSecrets.sort((x, y) => - sortCompareString(x.name, y.name) - ); - - type GroupedSecretMap = Record>; - const groupedSecrets = sortedData.reduce( - (obj, secret) => { - const key = secret.type || SecretType.SecretTypeGeneric; - return { ...obj, ...{ [key]: [...obj[key], secret] } }; - }, - Object.values(SecretType) - .sort((x, y) => sortCompareString(x, y)) - .reduce((obj, key) => ({ ...obj, [key]: [] }), {} as GroupedSecretMap) - ); - - const minimized = Object.keys(groupedSecrets).reduce>( - (obj, key) => [...obj, ...groupedSecrets[key]], - [] - ); - - console.log('sortedData', sortedData); - console.log('groupedSecrets', groupedSecrets); - console.log('moshpit', minimized); - - setSecrets(minimized); + setSecrets(componentSecrets); }, [secretNames, componentName, environment]); return ( diff --git a/src/components/component/secrets/secret-list-item-title-azure-key-vault-item.tsx b/src/components/component/secrets/secret-list-item-title-azure-key-vault-item.tsx index bb1cedd89..e7b652457 100644 --- a/src/components/component/secrets/secret-list-item-title-azure-key-vault-item.tsx +++ b/src/components/component/secrets/secret-list-item-title-azure-key-vault-item.tsx @@ -91,7 +91,6 @@ export const SecretListItemTitleAzureKeyVaultItem: FunctionComponent<
- {/* Splat! No sorting, just place and render... */} {filteredData.map((x, i) => ( ))} diff --git a/src/pages/page-application/index.tsx b/src/pages/page-application/index.tsx index 6d423f45f..2da2d7230 100644 --- a/src/pages/page-application/index.tsx +++ b/src/pages/page-application/index.tsx @@ -55,6 +55,7 @@ export const PageApplication: FunctionComponent = ({ resource="APP" resourceParams={[appName]} loadingContent={false} + errorContent={false} > {!application?.userIsAdmin && ( @@ -82,9 +83,11 @@ PageApplication.propTypes = { const ConnectedPageApplication = connect< PageApplicationState, - PageApplicationDispatch + PageApplicationDispatch, + PageApplicationProps, + RootState >( - (state: RootState) => ({ application: { ...getMemoizedApplication(state) } }), + (state) => ({ application: { ...getMemoizedApplication(state) } }), (dispatch) => ({ subscribeApp: (app) => dispatch(subscribeApplication(app)), unsubscribeApp: (app) => dispatch(unsubscribeApplication(app)),