mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-14 00:19:16 +01:00
refactor: improve feature not found page (#774)
* refactor: improve feature not found page * refactor: fix feature cache mutation mismatch
This commit is contained in:
parent
4066382b8f
commit
419f655ef5
@ -0,0 +1,7 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
featureId: {
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
},
|
||||||
|
}));
|
@ -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<IFeatureViewParams>();
|
||||||
|
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 (
|
||||||
|
<p>
|
||||||
|
The feature{' '}
|
||||||
|
<strong className={styles.featureId}>{featureId}</strong> has
|
||||||
|
been archived. You can find it on the{' '}
|
||||||
|
<Link to={'/archive'}>archive page</Link>.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
The feature{' '}
|
||||||
|
<strong className={styles.featureId}>{featureId}</strong> does not
|
||||||
|
exist. Would you like to{' '}
|
||||||
|
<Link to={createFeatureTogglePath}>create it</Link>?
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
};
|
@ -29,13 +29,12 @@ import { updateWeight } from '../../../../common/util';
|
|||||||
import cloneDeep from 'lodash.clonedeep';
|
import cloneDeep from 'lodash.clonedeep';
|
||||||
import useDeleteVariantMarkup from './FeatureVariantsListItem/useDeleteVariantMarkup';
|
import useDeleteVariantMarkup from './FeatureVariantsListItem/useDeleteVariantMarkup';
|
||||||
import PermissionButton from '../../../../common/PermissionButton/PermissionButton';
|
import PermissionButton from '../../../../common/PermissionButton/PermissionButton';
|
||||||
import { mutate } from 'swr';
|
|
||||||
import { formatUnknownError } from '../../../../../utils/format-unknown-error';
|
import { formatUnknownError } from '../../../../../utils/format-unknown-error';
|
||||||
|
|
||||||
const FeatureOverviewVariants = () => {
|
const FeatureOverviewVariants = () => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||||
const { feature, featureCacheKey } = useFeature(projectId, featureId);
|
const { feature, refetchFeature } = useFeature(projectId, featureId);
|
||||||
const [variants, setVariants] = useState<IFeatureVariant[]>([]);
|
const [variants, setVariants] = useState<IFeatureVariant[]>([]);
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const { context } = useUnleashContext();
|
const { context } = useUnleashContext();
|
||||||
@ -153,9 +152,8 @@ const FeatureOverviewVariants = () => {
|
|||||||
if (patch.length === 0) return;
|
if (patch.length === 0) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await patchFeatureVariants(projectId, featureId, patch);
|
await patchFeatureVariants(projectId, featureId, patch);
|
||||||
const { variants } = await res.json();
|
refetchFeature();
|
||||||
mutate(featureCacheKey, { ...feature, variants }, false);
|
|
||||||
setToastData({
|
setToastData({
|
||||||
title: 'Updated variant',
|
title: 'Updated variant',
|
||||||
confetti: true,
|
confetti: true,
|
||||||
@ -209,9 +207,8 @@ const FeatureOverviewVariants = () => {
|
|||||||
|
|
||||||
if (patch.length === 0) return;
|
if (patch.length === 0) return;
|
||||||
try {
|
try {
|
||||||
const res = await patchFeatureVariants(projectId, featureId, patch);
|
await patchFeatureVariants(projectId, featureId, patch);
|
||||||
const { variants } = await res.json();
|
refetchFeature();
|
||||||
mutate(featureCacheKey, { ...feature, variants }, false);
|
|
||||||
setToastData({
|
setToastData({
|
||||||
title: 'Updated variant',
|
title: 'Updated variant',
|
||||||
type: 'success',
|
type: 'success',
|
||||||
|
@ -44,7 +44,4 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
featureId: {
|
|
||||||
wordBreak: 'break-all',
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
@ -3,11 +3,11 @@ import React, { useState } from 'react';
|
|||||||
import { Archive, FileCopy, Label, WatchLater } from '@material-ui/icons';
|
import { Archive, FileCopy, Label, WatchLater } from '@material-ui/icons';
|
||||||
import { Link, Route, useHistory, useParams, Switch } from 'react-router-dom';
|
import { Link, Route, useHistory, useParams, Switch } from 'react-router-dom';
|
||||||
import useFeatureApi from '../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
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 useProject from '../../../hooks/api/getters/useProject/useProject';
|
||||||
import useTabs from '../../../hooks/useTabs';
|
import useTabs from '../../../hooks/useTabs';
|
||||||
import useToast from '../../../hooks/useToast';
|
import useToast from '../../../hooks/useToast';
|
||||||
import { IFeatureViewParams } from '../../../interfaces/params';
|
import { IFeatureViewParams } from 'interfaces/params';
|
||||||
import {
|
import {
|
||||||
CREATE_FEATURE,
|
CREATE_FEATURE,
|
||||||
DELETE_FEATURE,
|
DELETE_FEATURE,
|
||||||
@ -23,16 +23,14 @@ import { useStyles } from './FeatureView.styles';
|
|||||||
import { FeatureSettings } from './FeatureSettings/FeatureSettings';
|
import { FeatureSettings } from './FeatureSettings/FeatureSettings';
|
||||||
import useLoading from '../../../hooks/useLoading';
|
import useLoading from '../../../hooks/useLoading';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
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 StaleDialog from './FeatureOverview/StaleDialog/StaleDialog';
|
||||||
import AddTagDialog from './FeatureOverview/AddTagDialog/AddTagDialog';
|
import AddTagDialog from './FeatureOverview/AddTagDialog/AddTagDialog';
|
||||||
import StatusChip from '../../common/StatusChip/StatusChip';
|
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 = () => {
|
export const FeatureView = () => {
|
||||||
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
||||||
const { feature, loading, error } = useFeature(projectId, featureId);
|
|
||||||
const { refetch: projectRefetch } = useProject(projectId);
|
const { refetch: projectRefetch } = useProject(projectId);
|
||||||
const [openTagDialog, setOpenTagDialog] = useState(false);
|
const [openTagDialog, setOpenTagDialog] = useState(false);
|
||||||
const { a11yProps } = useTabs(0);
|
const { a11yProps } = useTabs(0);
|
||||||
@ -42,10 +40,14 @@ export const FeatureView = () => {
|
|||||||
const [openStaleDialog, setOpenStaleDialog] = useState(false);
|
const [openStaleDialog, setOpenStaleDialog] = useState(false);
|
||||||
const smallScreen = useMediaQuery(`(max-width:${500}px)`);
|
const smallScreen = useMediaQuery(`(max-width:${500}px)`);
|
||||||
|
|
||||||
|
const { feature, loading, error, status } = useFeature(
|
||||||
|
projectId,
|
||||||
|
featureId
|
||||||
|
);
|
||||||
|
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const ref = useLoading(loading);
|
const ref = useLoading(loading);
|
||||||
const { uiConfig } = useUiConfig();
|
|
||||||
|
|
||||||
const basePath = `/projects/${projectId}/features/${featureId}`;
|
const basePath = `/projects/${projectId}/features/${featureId}`;
|
||||||
|
|
||||||
@ -110,25 +112,9 @@ export const FeatureView = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderFeatureNotExist = () => {
|
if (status === 404) {
|
||||||
return (
|
return <FeatureNotFound />;
|
||||||
<div>
|
}
|
||||||
<p>
|
|
||||||
The feature{' '}
|
|
||||||
<strong className={styles.featureId}>{featureId}</strong>{' '}
|
|
||||||
does not exist. Do you want to
|
|
||||||
<Link
|
|
||||||
to={getCreateTogglePath(projectId, uiConfig.flags.E, {
|
|
||||||
name: featureId,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
create it
|
|
||||||
</Link>
|
|
||||||
?
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
@ -242,7 +228,6 @@ export const FeatureView = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
elseShow={renderFeatureNotExist()}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,18 +1,23 @@
|
|||||||
import useSWR, { mutate, SWRConfiguration } from 'swr';
|
import useSWR, { mutate, SWRConfiguration } from 'swr';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { IFeatureToggle } from '../../../../interfaces/featureToggle';
|
|
||||||
import { emptyFeature } from './emptyFeature';
|
import { emptyFeature } from './emptyFeature';
|
||||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
import { formatApiPath } from '../../../../utils/format-path';
|
import { formatApiPath } from 'utils/format-path';
|
||||||
|
import { IFeatureToggle } from 'interfaces/featureToggle';
|
||||||
|
|
||||||
interface IUseFeatureOutput {
|
interface IUseFeatureOutput {
|
||||||
feature: IFeatureToggle;
|
feature: IFeatureToggle;
|
||||||
featureCacheKey: string;
|
|
||||||
refetchFeature: () => void;
|
refetchFeature: () => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
status?: number;
|
||||||
error?: Error;
|
error?: Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IFeatureResponse {
|
||||||
|
status: number;
|
||||||
|
body?: IFeatureToggle;
|
||||||
|
}
|
||||||
|
|
||||||
export const useFeature = (
|
export const useFeature = (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
featureId: string,
|
featureId: string,
|
||||||
@ -22,7 +27,7 @@ export const useFeature = (
|
|||||||
`api/admin/projects/${projectId}/features/${featureId}`
|
`api/admin/projects/${projectId}/features/${featureId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data, error } = useSWR<IFeatureToggle>(
|
const { data, error } = useSWR<IFeatureResponse>(
|
||||||
path,
|
path,
|
||||||
() => fetcher(path),
|
() => fetcher(path),
|
||||||
options
|
options
|
||||||
@ -33,16 +38,27 @@ export const useFeature = (
|
|||||||
}, [path]);
|
}, [path]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
feature: data || emptyFeature,
|
feature: data?.body || emptyFeature,
|
||||||
featureCacheKey: path,
|
|
||||||
refetchFeature,
|
refetchFeature,
|
||||||
loading: !error && !data,
|
loading: !error && !data,
|
||||||
|
status: data?.status,
|
||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetcher = async (path: string) => {
|
const fetcher = async (path: string): Promise<IFeatureResponse> => {
|
||||||
return fetch(path)
|
const res = await fetch(path);
|
||||||
.then(handleErrorResponses('Feature toggle data'))
|
|
||||||
.then(res => res.json());
|
if (res.status === 404) {
|
||||||
|
return { status: 404 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
await handleErrorResponses('Feature toggle data')(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: res.status,
|
||||||
|
body: await res.json(),
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user