mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-06 00:07:44 +01:00
2d24d92f5a
* fix: fix UPDATE_PROJECT permission checks for editors * fix: disable delete button for the default project * fix: warn about access on edit project page * fix: hide broken project edit/delete buttons for OSS * refactor: avoid project card clicks when closing modals * refactor: improve default project deletion message * refactor: improve project access error text * Update src/component/project/ProjectCard/ProjectCard.tsx Co-authored-by: Thomas Heartman <thomas@getunleash.ai> * refactor: fix string quotes * refactor: improve disabled menu item contrast * refactor: remove Enterprise routes for OSS Co-authored-by: Thomas Heartman <thomas@getunleash.ai>
222 lines
8.0 KiB
TypeScript
222 lines
8.0 KiB
TypeScript
import { useContext, useEffect, useMemo, useState } from 'react';
|
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
|
import { mutate } from 'swr';
|
|
import { getProjectFetcher } from 'hooks/api/getters/useProject/getProjectFetcher';
|
|
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
import { ProjectCard } from '../ProjectCard/ProjectCard';
|
|
import { useStyles } from './ProjectList.styles';
|
|
import { IProjectCard } from 'interfaces/project';
|
|
import loadingData from './loadingData';
|
|
import useLoading from 'hooks/useLoading';
|
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
|
import AccessContext from 'contexts/AccessContext';
|
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
|
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
|
import { CREATE_PROJECT } from 'component/providers/AccessProvider/permissions';
|
|
import { Add } from '@mui/icons-material';
|
|
import ApiError from 'component/common/ApiError/ApiError';
|
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|
import { TablePlaceholder } from 'component/common/Table';
|
|
import { useMediaQuery } from '@mui/material';
|
|
import theme from 'themes/theme';
|
|
import { Search } from 'component/common/Search/Search';
|
|
|
|
type PageQueryType = Partial<Record<'search', string>>;
|
|
|
|
type projectMap = {
|
|
[index: string]: boolean;
|
|
};
|
|
|
|
function resolveCreateButtonData(isOss: boolean, hasAccess: boolean) {
|
|
if (isOss) {
|
|
return {
|
|
title: 'You must be on a paid subscription to create new projects',
|
|
disabled: true,
|
|
};
|
|
} else if (!hasAccess) {
|
|
return {
|
|
title: 'You do not have permission to create new projects',
|
|
disabled: true,
|
|
};
|
|
} else {
|
|
return {
|
|
title: 'Click to create a new project',
|
|
disabled: false,
|
|
};
|
|
}
|
|
}
|
|
|
|
export const ProjectListNew = () => {
|
|
const { hasAccess } = useContext(AccessContext);
|
|
const navigate = useNavigate();
|
|
const { classes: styles } = useStyles();
|
|
const { projects, loading, error, refetch } = useProjects();
|
|
const [fetchedProjects, setFetchedProjects] = useState<projectMap>({});
|
|
const ref = useLoading(loading);
|
|
const { isOss } = useUiConfig();
|
|
|
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const [searchValue, setSearchValue] = useState(
|
|
searchParams.get('search') || ''
|
|
);
|
|
|
|
useEffect(() => {
|
|
const tableState: PageQueryType = {};
|
|
if (searchValue) {
|
|
tableState.search = searchValue;
|
|
}
|
|
|
|
setSearchParams(tableState, {
|
|
replace: true,
|
|
});
|
|
}, [searchValue, setSearchParams]);
|
|
|
|
const filteredProjects = useMemo(() => {
|
|
const regExp = new RegExp(searchValue, 'i');
|
|
return searchValue
|
|
? projects.filter(project => regExp.test(project.name))
|
|
: projects;
|
|
}, [projects, searchValue]);
|
|
|
|
const handleHover = (projectId: string) => {
|
|
if (fetchedProjects[projectId]) {
|
|
return;
|
|
}
|
|
|
|
const { KEY, fetcher } = getProjectFetcher(projectId);
|
|
mutate(KEY, fetcher);
|
|
setFetchedProjects(prev => ({ ...prev, [projectId]: true }));
|
|
};
|
|
|
|
const createButtonData = resolveCreateButtonData(
|
|
isOss(),
|
|
hasAccess(CREATE_PROJECT)
|
|
);
|
|
|
|
const renderError = () => {
|
|
return (
|
|
<ApiError
|
|
onClick={refetch}
|
|
className={styles.apiError}
|
|
text="Error fetching projects"
|
|
/>
|
|
);
|
|
};
|
|
|
|
const renderProjects = () => {
|
|
if (loading) {
|
|
return renderLoading();
|
|
}
|
|
|
|
return filteredProjects.map((project: IProjectCard) => {
|
|
return (
|
|
<Link
|
|
key={project.id}
|
|
to={`/projects/${project.id}`}
|
|
className={styles.cardLink}
|
|
>
|
|
<ProjectCard
|
|
onHover={() => handleHover(project.id)}
|
|
name={project.name}
|
|
memberCount={project.memberCount ?? 0}
|
|
health={project.health}
|
|
id={project.id}
|
|
featureCount={project.featureCount}
|
|
/>
|
|
</Link>
|
|
);
|
|
});
|
|
};
|
|
|
|
const renderLoading = () => {
|
|
return loadingData.map((project: IProjectCard) => {
|
|
return (
|
|
<ProjectCard
|
|
data-loading
|
|
onHover={() => {}}
|
|
key={project.id}
|
|
name={project.name}
|
|
id={project.id}
|
|
memberCount={2}
|
|
health={95}
|
|
featureCount={4}
|
|
/>
|
|
);
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div ref={ref}>
|
|
<PageContent
|
|
header={
|
|
<PageHeader
|
|
title="Projects"
|
|
actions={
|
|
<>
|
|
<ConditionallyRender
|
|
condition={!isSmallScreen}
|
|
show={
|
|
<>
|
|
<Search
|
|
initialValue={searchValue}
|
|
onChange={setSearchValue}
|
|
/>
|
|
<PageHeader.Divider />
|
|
</>
|
|
}
|
|
/>
|
|
<ResponsiveButton
|
|
Icon={Add}
|
|
onClick={() => navigate('/projects/create')}
|
|
maxWidth="700px"
|
|
permission={CREATE_PROJECT}
|
|
disabled={createButtonData.disabled}
|
|
>
|
|
New project
|
|
</ResponsiveButton>
|
|
</>
|
|
}
|
|
>
|
|
<ConditionallyRender
|
|
condition={isSmallScreen}
|
|
show={
|
|
<Search
|
|
initialValue={searchValue}
|
|
onChange={setSearchValue}
|
|
/>
|
|
}
|
|
/>
|
|
</PageHeader>
|
|
}
|
|
>
|
|
<ConditionallyRender condition={error} show={renderError()} />
|
|
<div className={styles.container}>
|
|
<ConditionallyRender
|
|
condition={filteredProjects.length < 1 && !loading}
|
|
show={
|
|
<ConditionallyRender
|
|
condition={searchValue?.length > 0}
|
|
show={
|
|
<TablePlaceholder>
|
|
No projects found matching “
|
|
{searchValue}
|
|
”
|
|
</TablePlaceholder>
|
|
}
|
|
elseShow={
|
|
<TablePlaceholder>
|
|
No projects available.
|
|
</TablePlaceholder>
|
|
}
|
|
/>
|
|
}
|
|
elseShow={renderProjects()}
|
|
/>
|
|
</div>
|
|
</PageContent>
|
|
</div>
|
|
);
|
|
};
|