From 419f655ef5aadaa79cad789f98af1a9306edf765 Mon Sep 17 00:00:00 2001 From: olav Date: Thu, 10 Mar 2022 10:52:50 +0100 Subject: [PATCH] refactor: improve feature not found page (#774) * refactor: improve feature not found page * refactor: fix feature cache mutation mismatch --- .../FeatureNotFound/FeatureNotFound.styles.ts | 7 +++ .../FeatureNotFound/FeatureNotFound.tsx | 44 +++++++++++++++++++ .../FeatureVariantsList.tsx | 13 +++--- .../feature/FeatureView/FeatureView.styles.ts | 3 -- .../feature/FeatureView/FeatureView.tsx | 39 +++++----------- .../api/getters/useFeature/useFeature.ts | 36 ++++++++++----- 6 files changed, 94 insertions(+), 48 deletions(-) create mode 100644 frontend/src/component/feature/FeatureView/FeatureNotFound/FeatureNotFound.styles.ts create mode 100644 frontend/src/component/feature/FeatureView/FeatureNotFound/FeatureNotFound.tsx diff --git a/frontend/src/component/feature/FeatureView/FeatureNotFound/FeatureNotFound.styles.ts b/frontend/src/component/feature/FeatureView/FeatureNotFound/FeatureNotFound.styles.ts new file mode 100644 index 0000000000..a7cee2c317 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureNotFound/FeatureNotFound.styles.ts @@ -0,0 +1,7 @@ +import { makeStyles } from '@material-ui/core/styles'; + +export const useStyles = makeStyles(theme => ({ + featureId: { + wordBreak: 'break-all', + }, +})); diff --git a/frontend/src/component/feature/FeatureView/FeatureNotFound/FeatureNotFound.tsx b/frontend/src/component/feature/FeatureView/FeatureNotFound/FeatureNotFound.tsx new file mode 100644 index 0000000000..d1b13c4748 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureNotFound/FeatureNotFound.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Link, useParams } from 'react-router-dom'; +import { getCreateTogglePath } from 'utils/route-path-helpers'; +import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; +import { useStyles } from 'component/feature/FeatureView/FeatureNotFound/FeatureNotFound.styles'; +import { IFeatureViewParams } from 'interfaces/params'; +import { useFeaturesArchive } from 'hooks/api/getters/useFeaturesArchive/useFeaturesArchive'; + +export const FeatureNotFound = () => { + const { projectId, featureId } = useParams(); + const { archivedFeatures } = useFeaturesArchive(); + const styles = useStyles(); + const { uiConfig } = useUiConfig(); + + const createFeatureTogglePath = getCreateTogglePath( + projectId, + uiConfig.flags.E, + { name: featureId } + ); + + const isArchived = archivedFeatures.some(archivedFeature => { + return archivedFeature.name === featureId; + }); + + if (isArchived) { + return ( +

+ The feature{' '} + {featureId} has + been archived. You can find it on the{' '} + archive page. +

+ ); + } + + return ( +

+ The feature{' '} + {featureId} does not + exist. Would you like to{' '} + create it? +

+ ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureVariantsList/FeatureVariantsList.tsx b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureVariantsList/FeatureVariantsList.tsx index 55b419e900..21f528f5bd 100644 --- a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureVariantsList/FeatureVariantsList.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureVariantsList/FeatureVariantsList.tsx @@ -29,13 +29,12 @@ import { updateWeight } from '../../../../common/util'; import cloneDeep from 'lodash.clonedeep'; import useDeleteVariantMarkup from './FeatureVariantsListItem/useDeleteVariantMarkup'; import PermissionButton from '../../../../common/PermissionButton/PermissionButton'; -import { mutate } from 'swr'; import { formatUnknownError } from '../../../../../utils/format-unknown-error'; const FeatureOverviewVariants = () => { const { hasAccess } = useContext(AccessContext); const { projectId, featureId } = useParams(); - const { feature, featureCacheKey } = useFeature(projectId, featureId); + const { feature, refetchFeature } = useFeature(projectId, featureId); const [variants, setVariants] = useState([]); const [editing, setEditing] = useState(false); const { context } = useUnleashContext(); @@ -153,9 +152,8 @@ const FeatureOverviewVariants = () => { if (patch.length === 0) return; try { - const res = await patchFeatureVariants(projectId, featureId, patch); - const { variants } = await res.json(); - mutate(featureCacheKey, { ...feature, variants }, false); + await patchFeatureVariants(projectId, featureId, patch); + refetchFeature(); setToastData({ title: 'Updated variant', confetti: true, @@ -209,9 +207,8 @@ const FeatureOverviewVariants = () => { if (patch.length === 0) return; try { - const res = await patchFeatureVariants(projectId, featureId, patch); - const { variants } = await res.json(); - mutate(featureCacheKey, { ...feature, variants }, false); + await patchFeatureVariants(projectId, featureId, patch); + refetchFeature(); setToastData({ title: 'Updated variant', type: 'success', diff --git a/frontend/src/component/feature/FeatureView/FeatureView.styles.ts b/frontend/src/component/feature/FeatureView/FeatureView.styles.ts index 5a95b73106..3b59a9fd91 100644 --- a/frontend/src/component/feature/FeatureView/FeatureView.styles.ts +++ b/frontend/src/component/feature/FeatureView/FeatureView.styles.ts @@ -44,7 +44,4 @@ export const useStyles = makeStyles(theme => ({ flexDirection: 'column', }, }, - featureId: { - wordBreak: 'break-all', - }, })); diff --git a/frontend/src/component/feature/FeatureView/FeatureView.tsx b/frontend/src/component/feature/FeatureView/FeatureView.tsx index a93bcb5897..67e730d645 100644 --- a/frontend/src/component/feature/FeatureView/FeatureView.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureView.tsx @@ -3,11 +3,11 @@ import React, { useState } from 'react'; import { Archive, FileCopy, Label, WatchLater } from '@material-ui/icons'; import { Link, Route, useHistory, useParams, Switch } from 'react-router-dom'; import useFeatureApi from '../../../hooks/api/actions/useFeatureApi/useFeatureApi'; -import { useFeature } from '../../../hooks/api/getters/useFeature/useFeature'; +import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; import useProject from '../../../hooks/api/getters/useProject/useProject'; import useTabs from '../../../hooks/useTabs'; import useToast from '../../../hooks/useToast'; -import { IFeatureViewParams } from '../../../interfaces/params'; +import { IFeatureViewParams } from 'interfaces/params'; import { CREATE_FEATURE, DELETE_FEATURE, @@ -23,16 +23,14 @@ import { useStyles } from './FeatureView.styles'; import { FeatureSettings } from './FeatureSettings/FeatureSettings'; import useLoading from '../../../hooks/useLoading'; import ConditionallyRender from '../../common/ConditionallyRender'; -import { getCreateTogglePath } from '../../../utils/route-path-helpers'; -import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig'; import StaleDialog from './FeatureOverview/StaleDialog/StaleDialog'; import AddTagDialog from './FeatureOverview/AddTagDialog/AddTagDialog'; import StatusChip from '../../common/StatusChip/StatusChip'; -import { formatUnknownError } from '../../../utils/format-unknown-error'; +import { formatUnknownError } from 'utils/format-unknown-error'; +import { FeatureNotFound } from 'component/feature/FeatureView/FeatureNotFound/FeatureNotFound'; export const FeatureView = () => { const { projectId, featureId } = useParams(); - const { feature, loading, error } = useFeature(projectId, featureId); const { refetch: projectRefetch } = useProject(projectId); const [openTagDialog, setOpenTagDialog] = useState(false); const { a11yProps } = useTabs(0); @@ -42,10 +40,14 @@ export const FeatureView = () => { const [openStaleDialog, setOpenStaleDialog] = useState(false); const smallScreen = useMediaQuery(`(max-width:${500}px)`); + const { feature, loading, error, status } = useFeature( + projectId, + featureId + ); + const styles = useStyles(); const history = useHistory(); const ref = useLoading(loading); - const { uiConfig } = useUiConfig(); const basePath = `/projects/${projectId}/features/${featureId}`; @@ -110,25 +112,9 @@ export const FeatureView = () => { }); }; - const renderFeatureNotExist = () => { - return ( -
-

- The feature{' '} - {featureId}{' '} - does not exist. Do you want to   - - create it - -  ? -

-
- ); - }; + if (status === 404) { + return ; + } return ( { /> } - elseShow={renderFeatureNotExist()} /> ); }; diff --git a/frontend/src/hooks/api/getters/useFeature/useFeature.ts b/frontend/src/hooks/api/getters/useFeature/useFeature.ts index 771f8b4dbf..37aa26fd9d 100644 --- a/frontend/src/hooks/api/getters/useFeature/useFeature.ts +++ b/frontend/src/hooks/api/getters/useFeature/useFeature.ts @@ -1,18 +1,23 @@ import useSWR, { mutate, SWRConfiguration } from 'swr'; import { useCallback } from 'react'; -import { IFeatureToggle } from '../../../../interfaces/featureToggle'; import { emptyFeature } from './emptyFeature'; import handleErrorResponses from '../httpErrorResponseHandler'; -import { formatApiPath } from '../../../../utils/format-path'; +import { formatApiPath } from 'utils/format-path'; +import { IFeatureToggle } from 'interfaces/featureToggle'; interface IUseFeatureOutput { feature: IFeatureToggle; - featureCacheKey: string; refetchFeature: () => void; loading: boolean; + status?: number; error?: Error; } +interface IFeatureResponse { + status: number; + body?: IFeatureToggle; +} + export const useFeature = ( projectId: string, featureId: string, @@ -22,7 +27,7 @@ export const useFeature = ( `api/admin/projects/${projectId}/features/${featureId}` ); - const { data, error } = useSWR( + const { data, error } = useSWR( path, () => fetcher(path), options @@ -33,16 +38,27 @@ export const useFeature = ( }, [path]); return { - feature: data || emptyFeature, - featureCacheKey: path, + feature: data?.body || emptyFeature, refetchFeature, loading: !error && !data, + status: data?.status, error, }; }; -const fetcher = async (path: string) => { - return fetch(path) - .then(handleErrorResponses('Feature toggle data')) - .then(res => res.json()); +const fetcher = async (path: string): Promise => { + const res = await fetch(path); + + if (res.status === 404) { + return { status: 404 }; + } + + if (!res.ok) { + await handleErrorResponses('Feature toggle data')(res); + } + + return { + status: res.status, + body: await res.json(), + }; };