mirror of
https://github.com/Unleash/unleash.git
synced 2025-11-10 01:19:53 +01:00
424 lines
15 KiB
TypeScript
424 lines
15 KiB
TypeScript
import { useNavigate } from 'react-router';
|
|
import useLoading from 'hooks/useLoading';
|
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
import { ReactComponent as ProjectStatusSvg } from 'assets/icons/projectStatus.svg';
|
|
import {
|
|
StyledDiv,
|
|
StyledFavoriteIconButton,
|
|
StyledHeader,
|
|
StyledInnerContainer,
|
|
StyledName,
|
|
StyledProjectTitle,
|
|
StyledSeparator,
|
|
StyledTab,
|
|
StyledTabContainer,
|
|
StyledTopRow,
|
|
} from './Project.styles';
|
|
import {
|
|
Badge as CounterBadge,
|
|
Box,
|
|
Paper,
|
|
Tabs,
|
|
Typography,
|
|
styled,
|
|
Button,
|
|
} from '@mui/material';
|
|
import useToast from 'hooks/useToast';
|
|
import useQueryParams from 'hooks/useQueryParams';
|
|
import { useEffect, useState, type ReactNode } from 'react';
|
|
import ProjectEnvironment from '../ProjectEnvironment/ProjectEnvironment';
|
|
import { ProjectFeaturesArchive } from './ProjectFeaturesArchive/ProjectFeaturesArchive';
|
|
import ProjectFlags from './ProjectFlags';
|
|
import ProjectHealth from './ProjectHealth/ProjectHealth';
|
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|
import {
|
|
Navigate,
|
|
Route,
|
|
Routes,
|
|
useLocation,
|
|
useSearchParams,
|
|
} from 'react-router-dom';
|
|
import { DeleteProjectDialogue } from './DeleteProject/DeleteProjectDialogue';
|
|
import { ProjectLog } from './ProjectLog/ProjectLog';
|
|
import { ChangeRequestOverview } from 'component/changeRequest/ChangeRequestOverview/ChangeRequestOverview';
|
|
import { ProjectChangeRequests } from '../../changeRequest/ProjectChangeRequests/ProjectChangeRequests';
|
|
import { ProjectSettings } from './ProjectSettings/ProjectSettings';
|
|
import { useFavoriteProjectsApi } from 'hooks/api/actions/useFavoriteProjectsApi/useFavoriteProjectsApi';
|
|
import { ImportModal } from './Import/ImportModal';
|
|
import { EnterpriseBadge } from 'component/common/EnterpriseBadge/EnterpriseBadge';
|
|
import { Badge } from 'component/common/Badge/Badge';
|
|
import type { UiFlags } from 'interfaces/uiConfig';
|
|
import { HiddenProjectIconWithTooltip } from './HiddenProjectIconWithTooltip/HiddenProjectIconWithTooltip';
|
|
import { ChangeRequestPlausibleProvider } from 'component/changeRequest/ChangeRequestContext';
|
|
import { ProjectApplications } from '../ProjectApplications/ProjectApplications';
|
|
import { ProjectInsights } from './ProjectInsights/ProjectInsights';
|
|
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
|
import { ProjectArchived } from './ArchiveProject/ProjectArchived';
|
|
import { usePlausibleTracker } from '../../../hooks/usePlausibleTracker';
|
|
import { useActionableChangeRequests } from 'hooks/api/getters/useActionableChangeRequests/useActionableChangeRequests';
|
|
import { ProjectStatusModal } from './ProjectStatus/ProjectStatusModal';
|
|
|
|
const StyledBadge = styled(Badge)(({ theme }) => ({
|
|
position: 'absolute',
|
|
top: 5,
|
|
right: 20,
|
|
[theme.breakpoints.down('md')]: {
|
|
top: 2,
|
|
},
|
|
}));
|
|
|
|
interface ITab {
|
|
title: string;
|
|
path: string;
|
|
ossPath?: string;
|
|
name: string;
|
|
flag?: keyof UiFlags;
|
|
new?: boolean;
|
|
isEnterprise?: boolean;
|
|
labelOverride?: () => ReactNode;
|
|
}
|
|
|
|
const StyledCounterBadge = styled(CounterBadge)(({ theme }) => ({
|
|
'.MuiBadge-badge': {
|
|
backgroundColor: theme.palette.background.alternative,
|
|
right: '-4px',
|
|
},
|
|
[theme.breakpoints.down('md')]: {
|
|
right: '6px',
|
|
},
|
|
flex: 'auto',
|
|
justifyContent: 'center',
|
|
minHeight: '1.5em',
|
|
alignItems: 'center',
|
|
}));
|
|
|
|
const TabText = styled('span')(({ theme }) => ({
|
|
color: theme.palette.text.primary,
|
|
}));
|
|
|
|
const ChangeRequestsLabel = () => {
|
|
const projectId = useRequiredPathParam('projectId');
|
|
const { total } = useActionableChangeRequests(projectId);
|
|
|
|
return (
|
|
<StyledCounterBadge badgeContent={total ?? 0} color='primary'>
|
|
<TabText>Change requests</TabText>
|
|
</StyledCounterBadge>
|
|
);
|
|
};
|
|
|
|
const ProjectStatusButton = styled(Button)(({ theme }) => ({
|
|
color: theme.palette.text.primary,
|
|
fontSize: theme.typography.body1.fontSize,
|
|
fontWeight: 'bold',
|
|
'svg *': {
|
|
fill: theme.palette.primary.main,
|
|
},
|
|
}));
|
|
|
|
const ProjectStatusSvgWithMargin = styled(ProjectStatusSvg)(({ theme }) => ({
|
|
marginLeft: theme.spacing(0.5),
|
|
}));
|
|
|
|
const ProjectStatus = () => {
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const [projectStatusOpen, setProjectStatusOpen] = useState(
|
|
searchParams.has('project-status'),
|
|
);
|
|
const openStatusModal = () => {
|
|
searchParams.set('project-status', '');
|
|
setSearchParams(searchParams);
|
|
setProjectStatusOpen(true);
|
|
};
|
|
const closeStatusModal = () => {
|
|
searchParams.delete('project-status');
|
|
setSearchParams(searchParams);
|
|
setProjectStatusOpen(false);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<ProjectStatusButton
|
|
onClick={openStatusModal}
|
|
startIcon={<ProjectStatusSvgWithMargin />}
|
|
data-loading-project
|
|
>
|
|
Project status
|
|
</ProjectStatusButton>
|
|
<ProjectStatusModal
|
|
open={projectStatusOpen}
|
|
onClose={closeStatusModal}
|
|
onFollowLink={() => setProjectStatusOpen(false)}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const Project = () => {
|
|
const projectId = useRequiredPathParam('projectId');
|
|
const { trackEvent } = usePlausibleTracker();
|
|
const params = useQueryParams();
|
|
const { project, loading, error, refetch } = useProjectOverview(projectId);
|
|
const ref = useLoading(loading, '[data-loading-project=true]');
|
|
const { setToastData, setToastApiError } = useToast();
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const navigate = useNavigate();
|
|
const { pathname } = useLocation();
|
|
const { isOss, uiConfig, isPro } = useUiConfig();
|
|
const basePath = `/projects/${projectId}`;
|
|
const projectName = project?.name || projectId;
|
|
const { favorite, unfavorite } = useFavoriteProjectsApi();
|
|
|
|
const [showDelDialog, setShowDelDialog] = useState(false);
|
|
|
|
const [
|
|
changeRequestChangesWillOverwrite,
|
|
setChangeRequestChangesWillOverwrite,
|
|
] = useState(false);
|
|
|
|
const tabs: ITab[] = [
|
|
{
|
|
title: 'Overview',
|
|
path: basePath,
|
|
name: 'flags',
|
|
},
|
|
{
|
|
title: 'Change requests',
|
|
path: `${basePath}/change-requests`,
|
|
name: 'change-request',
|
|
isEnterprise: true,
|
|
labelOverride: ChangeRequestsLabel,
|
|
},
|
|
{
|
|
title: 'Applications',
|
|
path: `${basePath}/applications`,
|
|
name: 'applications',
|
|
},
|
|
{
|
|
title: 'Event log',
|
|
path: `${basePath}/logs`,
|
|
name: 'logs',
|
|
},
|
|
{
|
|
title: 'Settings',
|
|
path: `${basePath}/settings`,
|
|
ossPath: `${basePath}/settings/api-access`,
|
|
name: 'settings',
|
|
},
|
|
];
|
|
|
|
const filteredTabs = tabs
|
|
.filter((tab) => {
|
|
if (tab.flag) {
|
|
return uiConfig.flags[tab.flag];
|
|
}
|
|
return true;
|
|
})
|
|
.filter((tab) => !(isOss() && tab.isEnterprise));
|
|
|
|
const activeTab = [...filteredTabs]
|
|
.reverse()
|
|
.find((tab) => pathname.startsWith(tab.path));
|
|
|
|
useEffect(() => {
|
|
const created = params.get('created');
|
|
const edited = params.get('edited');
|
|
|
|
if (created || edited) {
|
|
const text = created ? 'Project created' : 'Project updated';
|
|
setToastData({
|
|
type: 'success',
|
|
text,
|
|
});
|
|
}
|
|
/* eslint-disable-next-line */
|
|
}, []);
|
|
|
|
if (error?.status === 404) {
|
|
return (
|
|
<Paper sx={(theme) => ({ padding: theme.spacing(2, 4, 4) })}>
|
|
<Typography variant='h1'>404 Not Found</Typography>
|
|
<Typography>
|
|
Project <strong>{projectId}</strong> does not exist.
|
|
</Typography>
|
|
</Paper>
|
|
);
|
|
}
|
|
|
|
const onFavorite = async () => {
|
|
try {
|
|
if (project?.favorite) {
|
|
await unfavorite(projectId);
|
|
} else {
|
|
await favorite(projectId);
|
|
}
|
|
refetch();
|
|
} catch (error) {
|
|
setToastApiError('Something went wrong, could not update favorite');
|
|
}
|
|
};
|
|
|
|
const enterpriseIcon = (
|
|
<Box
|
|
sx={(theme) => ({
|
|
marginLeft: theme.spacing(1),
|
|
display: 'flex',
|
|
})}
|
|
>
|
|
<EnterpriseBadge />
|
|
</Box>
|
|
);
|
|
|
|
if (project.archivedAt) {
|
|
return <ProjectArchived name={project.name} />;
|
|
}
|
|
|
|
return (
|
|
<div ref={ref}>
|
|
<StyledHeader>
|
|
<StyledInnerContainer>
|
|
<StyledTopRow>
|
|
<StyledDiv>
|
|
<StyledFavoriteIconButton
|
|
onClick={onFavorite}
|
|
isFavorite={project?.favorite}
|
|
/>
|
|
<StyledProjectTitle>
|
|
<ConditionallyRender
|
|
condition={project?.mode === 'private'}
|
|
show={<HiddenProjectIconWithTooltip />}
|
|
/>
|
|
<StyledName data-loading-project>
|
|
{projectName}
|
|
</StyledName>
|
|
</StyledProjectTitle>
|
|
</StyledDiv>
|
|
<StyledDiv>
|
|
<ProjectStatus />
|
|
</StyledDiv>
|
|
</StyledTopRow>
|
|
</StyledInnerContainer>
|
|
|
|
<StyledSeparator />
|
|
<StyledTabContainer>
|
|
<Tabs
|
|
value={activeTab?.path}
|
|
indicatorColor='primary'
|
|
textColor='primary'
|
|
variant='scrollable'
|
|
allowScrollButtonsMobile
|
|
>
|
|
{filteredTabs.map((tab) => {
|
|
return (
|
|
<StyledTab
|
|
data-loading-project
|
|
key={tab.title}
|
|
label={
|
|
tab.labelOverride ? (
|
|
<tab.labelOverride />
|
|
) : (
|
|
tab.title
|
|
)
|
|
}
|
|
value={tab.path}
|
|
onClick={() => {
|
|
if (tab.title !== 'Flags') {
|
|
trackEvent('project-navigation', {
|
|
props: {
|
|
eventType: tab.title,
|
|
},
|
|
});
|
|
}
|
|
navigate(
|
|
isOss() && tab.ossPath
|
|
? tab.ossPath
|
|
: tab.path,
|
|
);
|
|
}}
|
|
data-testid={`TAB_${tab.title}`}
|
|
iconPosition={
|
|
tab.isEnterprise ? 'end' : undefined
|
|
}
|
|
icon={
|
|
<>
|
|
<ConditionallyRender
|
|
condition={Boolean(tab.new)}
|
|
show={
|
|
// extra span to avoid badge getting color override from the overly specific parent component
|
|
<span>
|
|
<StyledBadge color='success'>
|
|
Beta
|
|
</StyledBadge>
|
|
</span>
|
|
}
|
|
/>
|
|
{(tab.isEnterprise &&
|
|
isPro() &&
|
|
enterpriseIcon) ||
|
|
undefined}
|
|
</>
|
|
}
|
|
/>
|
|
);
|
|
})}
|
|
</Tabs>
|
|
</StyledTabContainer>
|
|
</StyledHeader>
|
|
<DeleteProjectDialogue
|
|
projectId={projectId}
|
|
open={showDelDialog}
|
|
onClose={() => {
|
|
setShowDelDialog(false);
|
|
}}
|
|
onSuccess={() => {
|
|
navigate('/projects');
|
|
}}
|
|
/>
|
|
<Routes>
|
|
<Route path='health' element={<ProjectHealth />} />
|
|
<Route
|
|
path='access/*'
|
|
element={
|
|
<Navigate
|
|
replace
|
|
to={`/projects/${projectId}/settings/access`}
|
|
/>
|
|
}
|
|
/>
|
|
<Route path='environments' element={<ProjectEnvironment />} />
|
|
<Route path='archive' element={<ProjectFeaturesArchive />} />
|
|
<Route path='insights' element={<ProjectInsights />} />
|
|
<Route path='logs' element={<ProjectLog />} />
|
|
<Route
|
|
path='change-requests'
|
|
element={<ProjectChangeRequests />}
|
|
/>
|
|
<Route
|
|
path='change-requests/:id'
|
|
element={
|
|
<ChangeRequestPlausibleProvider
|
|
value={{
|
|
willOverwriteStrategyChanges:
|
|
changeRequestChangesWillOverwrite,
|
|
registerWillOverwriteStrategyChanges: () =>
|
|
setChangeRequestChangesWillOverwrite(true),
|
|
}}
|
|
>
|
|
<ChangeRequestOverview />
|
|
</ChangeRequestPlausibleProvider>
|
|
}
|
|
/>
|
|
<Route path='settings/*' element={<ProjectSettings />} />
|
|
<Route path='applications' element={<ProjectApplications />} />
|
|
<Route path='*' element={<ProjectFlags />} />
|
|
</Routes>
|
|
<ImportModal
|
|
open={modalOpen}
|
|
setOpen={setModalOpen}
|
|
project={projectId}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|