mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +02:00
chore!: remove project health report frontend (#10101)
This commit is contained in:
parent
d5acbea711
commit
7b0cae2b3e
@ -27,7 +27,6 @@ import useToast from 'hooks/useToast';
|
|||||||
import useQueryParams from 'hooks/useQueryParams';
|
import useQueryParams from 'hooks/useQueryParams';
|
||||||
import { useEffect, useState, type ReactNode } from 'react';
|
import { useEffect, useState, type ReactNode } from 'react';
|
||||||
import ProjectFlags from './ProjectFlags.tsx';
|
import ProjectFlags from './ProjectFlags.tsx';
|
||||||
import ProjectHealth from './ProjectHealth/ProjectHealth.tsx';
|
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import {
|
import {
|
||||||
@ -373,8 +372,6 @@ export const Project = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* FIXME: remove /health with `healthToTechDebt` flag - redirect to project status */}
|
|
||||||
<Route path='health' element={<ProjectHealth />} />
|
|
||||||
<Route
|
<Route
|
||||||
path='access/*'
|
path='access/*'
|
||||||
element={
|
element={
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
import { useHealthReport } from 'hooks/api/getters/useHealthReport/useHealthReport';
|
|
||||||
import ApiError from 'component/common/ApiError/ApiError';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import { usePageTitle } from 'hooks/usePageTitle';
|
|
||||||
import { ReportCard } from './ReportTable/ReportCard/ReportCard.tsx';
|
|
||||||
import { ReportTable } from './ReportTable/ReportTable.tsx';
|
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
|
||||||
import { useProjectOverviewNameOrId } from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
|
||||||
|
|
||||||
const ProjectHealth = () => {
|
|
||||||
const projectId = useRequiredPathParam('projectId');
|
|
||||||
const projectName = useProjectOverviewNameOrId(projectId);
|
|
||||||
usePageTitle(`Project health – ${projectName}`);
|
|
||||||
|
|
||||||
const { healthReport, refetchHealthReport, error } = useHealthReport(
|
|
||||||
projectId,
|
|
||||||
{ refreshInterval: 15 * 1000 },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!healthReport) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(error)}
|
|
||||||
show={
|
|
||||||
<ApiError
|
|
||||||
data-loading
|
|
||||||
style={{ maxWidth: '500px', marginTop: '1rem' }}
|
|
||||||
onClick={refetchHealthReport}
|
|
||||||
text={`Could not fetch health rating for ${projectId}`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ReportCard healthReport={healthReport} />
|
|
||||||
<ReportTable
|
|
||||||
projectId={projectId}
|
|
||||||
features={healthReport.features}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProjectHealth;
|
|
@ -1,219 +0,0 @@
|
|||||||
import { Box, Link, Paper, styled } from '@mui/material';
|
|
||||||
import CheckIcon from '@mui/icons-material/Check';
|
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
|
||||||
import ReportProblemOutlinedIcon from '@mui/icons-material/ReportProblemOutlined';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import type { IProjectHealthReport } from 'interfaces/project';
|
|
||||||
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
|
||||||
import InfoOutlined from '@mui/icons-material/InfoOutlined';
|
|
||||||
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
|
||||||
|
|
||||||
const StyledBoxActive = styled(Box)(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
color: theme.palette.success.dark,
|
|
||||||
'& svg': {
|
|
||||||
color: theme.palette.success.main,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledBoxStale = styled(Box)(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
color: theme.palette.warning.dark,
|
|
||||||
'& svg': {
|
|
||||||
color: theme.palette.warning.main,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledPaper = styled(Paper)(({ theme }) => ({
|
|
||||||
padding: theme.spacing(4),
|
|
||||||
marginBottom: theme.spacing(2),
|
|
||||||
borderRadius: theme.shape.borderRadiusLarge,
|
|
||||||
boxShadow: 'none',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
[theme.breakpoints.down('md')]: {
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: theme.spacing(2),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledHeader = styled('h2')(({ theme }) => ({
|
|
||||||
fontSize: theme.fontSizes.mainHeader,
|
|
||||||
marginBottom: theme.spacing(1),
|
|
||||||
justifyItems: 'center',
|
|
||||||
display: 'flex',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledHealthRating = styled('p')(({ theme }) => ({
|
|
||||||
fontSize: '2rem',
|
|
||||||
fontWeight: theme.fontWeight.bold,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledLastUpdated = styled('p')(({ theme }) => ({
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledList = styled('ul')(({ theme }) => ({
|
|
||||||
listStyleType: 'none',
|
|
||||||
margin: 0,
|
|
||||||
padding: 0,
|
|
||||||
'& svg': {
|
|
||||||
marginRight: theme.spacing(1),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledAlignedItem = styled('p')(({ theme }) => ({
|
|
||||||
marginLeft: theme.spacing(4),
|
|
||||||
}));
|
|
||||||
|
|
||||||
interface IReportCardProps {
|
|
||||||
healthReport: IProjectHealthReport;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ReportCard = ({ healthReport }: IReportCardProps) => {
|
|
||||||
const healthRatingColor =
|
|
||||||
healthReport.health < 50
|
|
||||||
? 'error.main'
|
|
||||||
: healthReport.health < 75
|
|
||||||
? 'warning.main'
|
|
||||||
: 'success.main';
|
|
||||||
|
|
||||||
const StalenessInfoIcon = () => (
|
|
||||||
<HtmlTooltip
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
If your flag exceeds the expected lifetime of its flag type
|
|
||||||
it will be marked as potentially stale.
|
|
||||||
<Box sx={{ mt: 2 }}>
|
|
||||||
<a
|
|
||||||
href='https://docs.getunleash.io/reference/technical-debt#stale-and-potentially-stale-flags'
|
|
||||||
target='_blank'
|
|
||||||
rel='noreferrer'
|
|
||||||
>
|
|
||||||
Read more in the documentation
|
|
||||||
</a>
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<InfoOutlined
|
|
||||||
sx={{ color: (theme) => theme.palette.text.secondary, ml: 1 }}
|
|
||||||
/>
|
|
||||||
</HtmlTooltip>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledPaper>
|
|
||||||
<Box>
|
|
||||||
<StyledHeader>Health rating</StyledHeader>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={healthReport.health > -1}
|
|
||||||
show={
|
|
||||||
<>
|
|
||||||
<StyledHealthRating
|
|
||||||
sx={{ color: healthRatingColor }}
|
|
||||||
>
|
|
||||||
{healthReport.health}%
|
|
||||||
</StyledHealthRating>
|
|
||||||
<StyledLastUpdated>
|
|
||||||
Last updated:{' '}
|
|
||||||
<TimeAgo
|
|
||||||
date={healthReport.updatedAt}
|
|
||||||
refresh={false}
|
|
||||||
/>
|
|
||||||
</StyledLastUpdated>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<StyledHeader>Flag report</StyledHeader>
|
|
||||||
<StyledList>
|
|
||||||
<li>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(healthReport.activeCount)}
|
|
||||||
show={
|
|
||||||
<StyledBoxActive>
|
|
||||||
<CheckIcon />
|
|
||||||
<span>
|
|
||||||
{healthReport.activeCount} active flags
|
|
||||||
</span>
|
|
||||||
</StyledBoxActive>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(healthReport.activeCount)}
|
|
||||||
show={
|
|
||||||
<StyledAlignedItem>
|
|
||||||
Also includes potentially stale flags.
|
|
||||||
</StyledAlignedItem>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(healthReport.staleCount)}
|
|
||||||
show={
|
|
||||||
<StyledBoxStale>
|
|
||||||
<ReportProblemOutlinedIcon />
|
|
||||||
<span>
|
|
||||||
{healthReport.staleCount} stale flags
|
|
||||||
</span>
|
|
||||||
</StyledBoxStale>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</StyledList>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{ flexBasis: '40%' }}>
|
|
||||||
<StyledHeader>
|
|
||||||
Potential actions{' '}
|
|
||||||
<span>
|
|
||||||
<StalenessInfoIcon />
|
|
||||||
</span>
|
|
||||||
</StyledHeader>
|
|
||||||
<StyledList>
|
|
||||||
<li>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(
|
|
||||||
healthReport.potentiallyStaleCount,
|
|
||||||
)}
|
|
||||||
show={
|
|
||||||
<StyledBoxStale>
|
|
||||||
<ReportProblemOutlinedIcon />
|
|
||||||
<span>
|
|
||||||
{healthReport.potentiallyStaleCount}{' '}
|
|
||||||
potentially stale flags
|
|
||||||
</span>
|
|
||||||
</StyledBoxStale>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</StyledList>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(healthReport.potentiallyStaleCount)}
|
|
||||||
show={
|
|
||||||
<>
|
|
||||||
<StyledAlignedItem>
|
|
||||||
Review your feature flags and delete unused
|
|
||||||
flags.
|
|
||||||
</StyledAlignedItem>
|
|
||||||
<Box sx={{ mt: 2 }}>
|
|
||||||
<Link
|
|
||||||
component={RouterLink}
|
|
||||||
to={'/feature-toggle-type'}
|
|
||||||
>
|
|
||||||
Configure feature types lifetime
|
|
||||||
</Link>
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
elseShow={<span>No action is required</span>}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</StyledPaper>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,27 +0,0 @@
|
|||||||
import type { VFC } from 'react';
|
|
||||||
import { Typography, useTheme } from '@mui/material';
|
|
||||||
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
|
||||||
import type { IReportTableRow } from 'component/project/Project/ProjectHealth/ReportTable/ReportTable';
|
|
||||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
|
||||||
|
|
||||||
interface IReportExpiredCellProps {
|
|
||||||
row: {
|
|
||||||
original: IReportTableRow;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ReportExpiredCell: VFC<IReportExpiredCellProps> = ({ row }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
if (row.original.expiredAt) {
|
|
||||||
return <DateCell value={row.original.expiredAt} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextCell>
|
|
||||||
<Typography variant='body2' color={theme.palette.text.secondary}>
|
|
||||||
N/A
|
|
||||||
</Typography>
|
|
||||||
</TextCell>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,34 +0,0 @@
|
|||||||
import type { IFeatureFlagListItem } from 'interfaces/featureToggle';
|
|
||||||
import { KILLSWITCH, PERMISSION } from 'constants/featureToggleTypes';
|
|
||||||
import { expired, getDiffInDays } from '../utils.js';
|
|
||||||
import { parseISO, subDays } from 'date-fns';
|
|
||||||
import type { FeatureTypeSchema } from 'openapi';
|
|
||||||
|
|
||||||
export const formatExpiredAt = (
|
|
||||||
feature: IFeatureFlagListItem,
|
|
||||||
featureTypes: FeatureTypeSchema[],
|
|
||||||
): string | undefined => {
|
|
||||||
const { type, createdAt } = feature;
|
|
||||||
|
|
||||||
const featureType = featureTypes.find(
|
|
||||||
(featureType) => featureType.id === type,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
featureType &&
|
|
||||||
(featureType.name === KILLSWITCH || featureType.name === PERMISSION)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const date = parseISO(createdAt);
|
|
||||||
const now = new Date();
|
|
||||||
const diff = getDiffInDays(date, now);
|
|
||||||
|
|
||||||
if (featureType && expired(diff, featureType)) {
|
|
||||||
const result = diff - (featureType?.lifetimeDays?.valueOf() || 0);
|
|
||||||
return subDays(now, result).toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
};
|
|
@ -1,52 +0,0 @@
|
|||||||
import type { VFC, ReactElement } from 'react';
|
|
||||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
|
||||||
import Check from '@mui/icons-material/Check';
|
|
||||||
import ReportProblemOutlined from '@mui/icons-material/ReportProblemOutlined';
|
|
||||||
import { styled } from '@mui/material';
|
|
||||||
import type { IReportTableRow } from 'component/project/Project/ProjectHealth/ReportTable/ReportTable';
|
|
||||||
|
|
||||||
const StyledTextPotentiallyStale = styled('span')(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
gap: '1ch',
|
|
||||||
alignItems: 'center',
|
|
||||||
color: theme.palette.warning.dark,
|
|
||||||
'& svg': { color: theme.palette.warning.main },
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledTextHealthy = styled('span')(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
gap: '1ch',
|
|
||||||
alignItems: 'center',
|
|
||||||
color: theme.palette.success.dark,
|
|
||||||
'& svg': { color: theme.palette.success.main },
|
|
||||||
}));
|
|
||||||
|
|
||||||
interface IReportStatusCellProps {
|
|
||||||
row: {
|
|
||||||
original: IReportTableRow;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ReportStatusCell: VFC<IReportStatusCellProps> = ({
|
|
||||||
row,
|
|
||||||
}): ReactElement => {
|
|
||||||
if (row.original.status === 'potentially-stale') {
|
|
||||||
return (
|
|
||||||
<TextCell>
|
|
||||||
<StyledTextPotentiallyStale>
|
|
||||||
<ReportProblemOutlined />
|
|
||||||
<span>Potentially stale</span>
|
|
||||||
</StyledTextPotentiallyStale>
|
|
||||||
</TextCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextCell>
|
|
||||||
<StyledTextHealthy>
|
|
||||||
<Check />
|
|
||||||
<span>Healthy</span>
|
|
||||||
</StyledTextHealthy>
|
|
||||||
</TextCell>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,32 +0,0 @@
|
|||||||
import type { IFeatureFlagListItem } from 'interfaces/featureToggle';
|
|
||||||
import { expired, getDiffInDays } from '../utils.js';
|
|
||||||
import { KILLSWITCH, PERMISSION } from 'constants/featureToggleTypes';
|
|
||||||
import { parseISO } from 'date-fns';
|
|
||||||
import type { FeatureTypeSchema } from 'openapi';
|
|
||||||
|
|
||||||
export type ReportingStatus = 'potentially-stale' | 'healthy';
|
|
||||||
|
|
||||||
export const formatStatus = (
|
|
||||||
feature: IFeatureFlagListItem,
|
|
||||||
featureTypes: FeatureTypeSchema[],
|
|
||||||
): ReportingStatus => {
|
|
||||||
const { type, createdAt } = feature;
|
|
||||||
|
|
||||||
const featureType = featureTypes.find(
|
|
||||||
(featureType) => featureType.id === type,
|
|
||||||
);
|
|
||||||
const date = parseISO(createdAt);
|
|
||||||
const now = new Date();
|
|
||||||
const diff = getDiffInDays(date, now);
|
|
||||||
|
|
||||||
if (
|
|
||||||
featureType &&
|
|
||||||
expired(diff, featureType) &&
|
|
||||||
type !== KILLSWITCH &&
|
|
||||||
type !== PERMISSION
|
|
||||||
) {
|
|
||||||
return 'potentially-stale';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'healthy';
|
|
||||||
};
|
|
@ -1,237 +0,0 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import type {
|
|
||||||
IEnvironments,
|
|
||||||
IFeatureFlagListItem,
|
|
||||||
} from 'interfaces/featureToggle';
|
|
||||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
|
||||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
|
||||||
import { sortTypes } from 'utils/sortTypes';
|
|
||||||
import {
|
|
||||||
useFlexLayout,
|
|
||||||
useGlobalFilter,
|
|
||||||
useSortBy,
|
|
||||||
useTable,
|
|
||||||
} from 'react-table';
|
|
||||||
import { useMediaQuery, useTheme } from '@mui/material';
|
|
||||||
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
|
|
||||||
import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell';
|
|
||||||
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
|
||||||
import { FeatureStaleCell } from 'component/feature/FeatureToggleList/FeatureStaleCell/FeatureStaleCell';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import { Search } from 'component/common/Search/Search';
|
|
||||||
import { ReportExpiredCell } from './ReportExpiredCell/ReportExpiredCell.tsx';
|
|
||||||
import { ReportStatusCell } from './ReportStatusCell/ReportStatusCell.tsx';
|
|
||||||
import {
|
|
||||||
formatStatus,
|
|
||||||
type ReportingStatus,
|
|
||||||
} from './ReportStatusCell/formatStatus.ts';
|
|
||||||
import { formatExpiredAt } from './ReportExpiredCell/formatExpiredAt.ts';
|
|
||||||
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|
||||||
import { FeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
|
|
||||||
import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes';
|
|
||||||
|
|
||||||
interface IReportTableProps {
|
|
||||||
projectId: string;
|
|
||||||
features: IFeatureFlagListItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IReportTableRow {
|
|
||||||
project: string;
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
stale?: boolean;
|
|
||||||
status: ReportingStatus;
|
|
||||||
lastSeenAt?: string;
|
|
||||||
environments?: IEnvironments[];
|
|
||||||
createdAt: string;
|
|
||||||
expiredAt?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ReportTable = ({ projectId, features }: IReportTableProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
|
||||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
|
||||||
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
|
|
||||||
const { uiConfig } = useUiConfig();
|
|
||||||
|
|
||||||
const { featureTypes } = useFeatureTypes();
|
|
||||||
|
|
||||||
const data: IReportTableRow[] = useMemo<IReportTableRow[]>(
|
|
||||||
() =>
|
|
||||||
features.map((report) => ({
|
|
||||||
project: projectId,
|
|
||||||
name: report.name,
|
|
||||||
type: report.type,
|
|
||||||
stale: report.stale,
|
|
||||||
environments: report.environments,
|
|
||||||
status: formatStatus(report, featureTypes),
|
|
||||||
lastSeenAt: report.lastSeenAt,
|
|
||||||
createdAt: report.createdAt,
|
|
||||||
expiredAt: formatExpiredAt(report, featureTypes),
|
|
||||||
})),
|
|
||||||
[projectId, features, featureTypes],
|
|
||||||
);
|
|
||||||
|
|
||||||
const initialState = useMemo(
|
|
||||||
() => ({
|
|
||||||
hiddenColumns: [],
|
|
||||||
sortBy: [{ id: 'createdAt', desc: true }],
|
|
||||||
}),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const COLUMNS = useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
Header: 'Seen',
|
|
||||||
accessor: 'lastSeenAt',
|
|
||||||
Cell: ({ value, row: { original: feature } }: any) => {
|
|
||||||
return <FeatureEnvironmentSeenCell feature={feature} />;
|
|
||||||
},
|
|
||||||
align: 'center',
|
|
||||||
maxWidth: 80,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: 'Type',
|
|
||||||
accessor: 'type',
|
|
||||||
align: 'center',
|
|
||||||
Cell: FeatureTypeCell,
|
|
||||||
disableGlobalFilter: true,
|
|
||||||
maxWidth: 85,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: 'Name',
|
|
||||||
accessor: 'name',
|
|
||||||
sortType: 'alphanumeric',
|
|
||||||
Cell: FeatureNameCell,
|
|
||||||
minWidth: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: 'Created',
|
|
||||||
accessor: 'createdAt',
|
|
||||||
Cell: DateCell,
|
|
||||||
disableGlobalFilter: true,
|
|
||||||
maxWidth: 150,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: 'Expired',
|
|
||||||
accessor: 'expiredAt',
|
|
||||||
Cell: ReportExpiredCell,
|
|
||||||
disableGlobalFilter: true,
|
|
||||||
maxWidth: 150,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: 'Status',
|
|
||||||
id: 'status',
|
|
||||||
accessor: 'status',
|
|
||||||
Cell: ReportStatusCell,
|
|
||||||
disableGlobalFilter: true,
|
|
||||||
width: 180,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: 'State',
|
|
||||||
accessor: 'stale',
|
|
||||||
sortType: 'boolean',
|
|
||||||
Cell: FeatureStaleCell,
|
|
||||||
disableGlobalFilter: true,
|
|
||||||
maxWidth: 120,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
headerGroups,
|
|
||||||
rows,
|
|
||||||
prepareRow,
|
|
||||||
state: { globalFilter },
|
|
||||||
setGlobalFilter,
|
|
||||||
setHiddenColumns,
|
|
||||||
} = useTable(
|
|
||||||
{
|
|
||||||
columns: COLUMNS as any,
|
|
||||||
data: data as any,
|
|
||||||
initialState,
|
|
||||||
sortTypes,
|
|
||||||
autoResetGlobalFilter: false,
|
|
||||||
autoResetHiddenColumns: false,
|
|
||||||
autoResetSortBy: false,
|
|
||||||
disableSortRemove: true,
|
|
||||||
},
|
|
||||||
useGlobalFilter,
|
|
||||||
useFlexLayout,
|
|
||||||
useSortBy,
|
|
||||||
);
|
|
||||||
|
|
||||||
useConditionallyHiddenColumns(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
condition: isExtraSmallScreen,
|
|
||||||
columns: ['stale'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
condition: isSmallScreen,
|
|
||||||
columns: ['expiredAt', 'lastSeenAt'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
condition: isMediumScreen,
|
|
||||||
columns: ['createdAt'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
setHiddenColumns,
|
|
||||||
COLUMNS,
|
|
||||||
);
|
|
||||||
|
|
||||||
const title =
|
|
||||||
rows.length < data.length
|
|
||||||
? `Feature flags (${rows.length} of ${data.length})`
|
|
||||||
: `Feature flags (${data.length})`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent
|
|
||||||
header={
|
|
||||||
<PageHeader
|
|
||||||
title={title}
|
|
||||||
actions={
|
|
||||||
<Search
|
|
||||||
initialValue={globalFilter}
|
|
||||||
onChange={setGlobalFilter}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SearchHighlightProvider value={globalFilter}>
|
|
||||||
<VirtualizedTable
|
|
||||||
headerGroups={headerGroups}
|
|
||||||
prepareRow={prepareRow}
|
|
||||||
rows={rows}
|
|
||||||
/>
|
|
||||||
</SearchHighlightProvider>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={rows.length === 0}
|
|
||||||
show={
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={globalFilter?.length > 0}
|
|
||||||
show={
|
|
||||||
<TablePlaceholder>
|
|
||||||
No feature flags found matching “
|
|
||||||
{globalFilter}
|
|
||||||
”
|
|
||||||
</TablePlaceholder>
|
|
||||||
}
|
|
||||||
elseShow={
|
|
||||||
<TablePlaceholder>
|
|
||||||
No feature flags available. Get started by
|
|
||||||
adding a new feature flag.
|
|
||||||
</TablePlaceholder>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,12 +0,0 @@
|
|||||||
import differenceInDays from 'date-fns/differenceInDays';
|
|
||||||
import type { FeatureTypeSchema } from 'openapi';
|
|
||||||
|
|
||||||
export const getDiffInDays = (date: Date, now: Date) => {
|
|
||||||
return Math.abs(differenceInDays(date, now));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const expired = (diff: number, type: FeatureTypeSchema) => {
|
|
||||||
if (type.lifetimeDays) return diff >= type?.lifetimeDays?.valueOf();
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
@ -1,42 +0,0 @@
|
|||||||
import useSWR, { mutate, type SWRConfiguration } from 'swr';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import type { IProjectHealthReport } from 'interfaces/project';
|
|
||||||
import { formatApiPath } from 'utils/formatPath';
|
|
||||||
import handleErrorResponses from '../httpErrorResponseHandler.js';
|
|
||||||
|
|
||||||
interface IUseHealthReportOutput {
|
|
||||||
healthReport: IProjectHealthReport | undefined;
|
|
||||||
refetchHealthReport: () => void;
|
|
||||||
loading: boolean;
|
|
||||||
error?: Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useHealthReport = (
|
|
||||||
projectId: string,
|
|
||||||
options?: SWRConfiguration,
|
|
||||||
): IUseHealthReportOutput => {
|
|
||||||
const path = formatApiPath(`api/admin/projects/${projectId}/health-report`);
|
|
||||||
|
|
||||||
const { data, error } = useSWR<IProjectHealthReport>(
|
|
||||||
path,
|
|
||||||
fetchHealthReport,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
const refetchHealthReport = useCallback(() => {
|
|
||||||
mutate(path).catch(console.warn);
|
|
||||||
}, [path]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
healthReport: data,
|
|
||||||
refetchHealthReport,
|
|
||||||
loading: !error && !data,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchHealthReport = (path: string): Promise<IProjectHealthReport> => {
|
|
||||||
return fetch(path)
|
|
||||||
.then(handleErrorResponses('Health report'))
|
|
||||||
.then((res) => res.json());
|
|
||||||
};
|
|
Loading…
Reference in New Issue
Block a user