1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-06 01:15:28 +02:00

Merge branch 'main' into archive_table

This commit is contained in:
Fredrik Strand Oseberg 2022-06-10 16:22:07 +02:00 committed by GitHub
commit bd4651b9ca
13 changed files with 77 additions and 17 deletions

View File

@ -13,15 +13,21 @@ import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
import { SplashPageRedirect } from 'component/splash/SplashPageRedirect/SplashPageRedirect'; import { SplashPageRedirect } from 'component/splash/SplashPageRedirect/SplashPageRedirect';
import { useStyles } from './App.styles'; import { useStyles } from './App.styles';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
export const App = () => { export const App = () => {
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
const { authDetails } = useAuthDetails(); const { authDetails } = useAuthDetails();
const { user } = useAuthUser(); const { user } = useAuthUser();
const { isOss } = useUiConfig();
const isLoggedIn = Boolean(user?.id); const isLoggedIn = Boolean(user?.id);
const hasFetchedAuth = Boolean(authDetails || user); const hasFetchedAuth = Boolean(authDetails || user);
usePlausibleTracker(); usePlausibleTracker();
const availableRoutes = isOss()
? routes.filter(route => !route.enterprise)
: routes;
return ( return (
<SWRProvider isUnauthorized={!isLoggedIn}> <SWRProvider isUnauthorized={!isLoggedIn}>
<ConditionallyRender <ConditionallyRender
@ -32,7 +38,7 @@ export const App = () => {
<ToastRenderer /> <ToastRenderer />
<LayoutPicker> <LayoutPicker>
<Routes> <Routes>
{routes.map(route => ( {availableRoutes.map(route => (
<Route <Route
key={route.path} key={route.path}
path={route.path} path={route.path}

View File

@ -20,6 +20,7 @@ interface IPermissionIconButtonProps {
type?: 'button'; type?: 'button';
edge?: IconButtonProps['edge']; edge?: IconButtonProps['edge'];
tooltipProps?: Omit<ITooltipResolverProps, 'children'>; tooltipProps?: Omit<ITooltipResolverProps, 'children'>;
sx?: IconButtonProps['sx'];
size?: string; size?: string;
} }

View File

@ -11,6 +11,7 @@ exports[`returns all baseRoutes 1`] = `
}, },
{ {
"component": [Function], "component": [Function],
"enterprise": true,
"menu": {}, "menu": {},
"parent": "/projects", "parent": "/projects",
"path": "/projects/create", "path": "/projects/create",
@ -19,6 +20,7 @@ exports[`returns all baseRoutes 1`] = `
}, },
{ {
"component": [Function], "component": [Function],
"enterprise": true,
"menu": {}, "menu": {},
"parent": "/projects", "parent": "/projects",
"path": "/projects/:projectId/edit", "path": "/projects/:projectId/edit",

View File

@ -70,6 +70,7 @@ export const routes: IRoute[] = [
title: 'Create', title: 'Create',
component: CreateProject, component: CreateProject,
type: 'protected', type: 'protected',
enterprise: true,
menu: {}, menu: {},
}, },
{ {
@ -78,6 +79,7 @@ export const routes: IRoute[] = [
title: ':projectId', title: ':projectId',
component: EditProject, component: EditProject,
type: 'protected', type: 'protected',
enterprise: true,
menu: {}, menu: {},
}, },
{ {

View File

@ -10,13 +10,18 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useContext } from 'react';
import AccessContext from 'contexts/AccessContext';
import { Alert } from '@mui/material';
const EditProject = () => { const EditProject = () => {
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const { hasAccess } = useContext(AccessContext);
const id = useRequiredPathParam('projectId'); const id = useRequiredPathParam('projectId');
const { project } = useProject(id); const { project } = useProject(id);
const navigate = useNavigate(); const navigate = useNavigate();
const { const {
projectId, projectId,
projectName, projectName,
@ -68,6 +73,12 @@ const EditProject = () => {
navigate(-1); navigate(-1);
}; };
const accessDeniedAlert = !hasAccess(UPDATE_PROJECT, projectId) && (
<Alert severity="error" sx={{ mb: 4 }}>
You do not have the required permissions to edit this project.
</Alert>
);
return ( return (
<FormTemplate <FormTemplate
loading={loading} loading={loading}
@ -77,6 +88,7 @@ const EditProject = () => {
documentationLinkLabel="Projects documentation" documentationLinkLabel="Projects documentation"
formatApiCode={formatApiCode} formatApiCode={formatApiCode}
> >
{accessDeniedAlert}
<ProjectForm <ProjectForm
errors={errors} errors={errors}
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
@ -91,7 +103,10 @@ const EditProject = () => {
clearErrors={clearErrors} clearErrors={clearErrors}
validateProjectId={validateProjectId} validateProjectId={validateProjectId}
> >
<UpdateButton permission={UPDATE_PROJECT} /> <UpdateButton
permission={UPDATE_PROJECT}
projectId={projectId}
/>
</ProjectForm> </ProjectForm>
</FormTemplate> </FormTemplate>
); );

View File

@ -19,6 +19,7 @@ import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel'; import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useOptionalPathParam } from 'hooks/useOptionalPathParam'; import { useOptionalPathParam } from 'hooks/useOptionalPathParam';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
const Project = () => { const Project = () => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
@ -29,6 +30,7 @@ const Project = () => {
const { setToastData } = useToast(); const { setToastData } = useToast();
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
const navigate = useNavigate(); const navigate = useNavigate();
const { isOss } = useUiConfig();
const basePath = `/projects/${projectId}`; const basePath = `/projects/${projectId}`;
const tabData = [ const tabData = [
@ -118,7 +120,8 @@ const Project = () => {
</div> </div>
<PermissionIconButton <PermissionIconButton
permission={UPDATE_PROJECT} permission={UPDATE_PROJECT}
projectId={project?.id} projectId={projectId}
sx={{ visibility: isOss() ? 'hidden' : 'visible' }}
onClick={() => onClick={() =>
navigate(`/projects/${projectId}/edit`) navigate(`/projects/${projectId}/edit`)
} }

View File

@ -33,7 +33,7 @@ const ProjectInfo = ({
}: IProjectInfoProps) => { }: IProjectInfoProps) => {
const { classes: themeStyles } = useThemeStyles(); const { classes: themeStyles } = useThemeStyles();
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
const { uiConfig } = useUiConfig(); const { uiConfig, isOss } = useUiConfig();
let link = `/admin/users`; let link = `/admin/users`;
@ -52,6 +52,7 @@ const ProjectInfo = ({
<div> <div>
<PermissionIconButton <PermissionIconButton
permission={UPDATE_PROJECT} permission={UPDATE_PROJECT}
hidden={isOss()}
projectId={id} projectId={id}
component={Link} component={Link}
className={permissionButtonClass} className={permissionButtonClass}

View File

@ -2,7 +2,7 @@ import { Card, Menu, MenuItem } from '@mui/material';
import { useStyles } from './ProjectCard.styles'; import { useStyles } from './ProjectCard.styles';
import MoreVertIcon from '@mui/icons-material/MoreVert'; import MoreVertIcon from '@mui/icons-material/MoreVert';
import { ReactComponent as ProjectIcon } from 'assets/icons/projectIcon.svg'; import { ReactComponent as ProjectIcon } from 'assets/icons/projectIcon.svg';
import { useState, SyntheticEvent } from 'react'; import React, { useState, SyntheticEvent, useContext } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { Dialogue } from 'component/common/Dialogue/Dialogue';
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi'; import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
@ -11,8 +11,14 @@ import { Delete, Edit } from '@mui/icons-material';
import { getProjectEditPath } from 'utils/routePathHelpers'; import { getProjectEditPath } from 'utils/routePathHelpers';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions'; import {
UPDATE_PROJECT,
DELETE_PROJECT,
} from 'component/providers/AccessProvider/permissions';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import AccessContext from 'contexts/AccessContext';
import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
interface IProjectCardProps { interface IProjectCardProps {
name: string; name: string;
@ -32,6 +38,8 @@ export const ProjectCard = ({
id, id,
}: IProjectCardProps) => { }: IProjectCardProps) => {
const { classes } = useStyles(); const { classes } = useStyles();
const { hasAccess } = useContext(AccessContext);
const { isOss } = useUiConfig();
const { refetch: refetchProjectOverview } = useProjects(); const { refetch: refetchProjectOverview } = useProjects();
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
const [showDelDialog, setShowDelDialog] = useState(false); const [showDelDialog, setShowDelDialog] = useState(false);
@ -45,7 +53,7 @@ export const ProjectCard = ({
setAnchorEl(e.currentTarget); setAnchorEl(e.currentTarget);
}; };
const onRemoveProject = async (e: Event) => { const onRemoveProject = async (e: React.SyntheticEvent) => {
e.preventDefault(); e.preventDefault();
try { try {
await deleteProject(id); await deleteProject(id);
@ -62,6 +70,9 @@ export const ProjectCard = ({
setAnchorEl(null); setAnchorEl(null);
}; };
const canDeleteProject =
hasAccess(DELETE_PROJECT, id) && id !== DEFAULT_PROJECT_ID;
return ( return (
<Card className={classes.projectCard} onMouseEnter={onHover}> <Card className={classes.projectCard} onMouseEnter={onHover}>
<div className={classes.header} data-loading> <div className={classes.header} data-loading>
@ -69,6 +80,7 @@ export const ProjectCard = ({
<PermissionIconButton <PermissionIconButton
permission={UPDATE_PROJECT} permission={UPDATE_PROJECT}
hidden={isOss()}
projectId={id} projectId={id}
data-loading data-loading
onClick={handleClick} onClick={handleClick}
@ -85,6 +97,9 @@ export const ProjectCard = ({
open={Boolean(anchorEl)} open={Boolean(anchorEl)}
anchorEl={anchorEl} anchorEl={anchorEl}
style={{ top: 0, left: -100 }} style={{ top: 0, left: -100 }}
onClick={event => {
event.preventDefault();
}}
onClose={(event: SyntheticEvent) => { onClose={(event: SyntheticEvent) => {
event.preventDefault(); event.preventDefault();
setAnchorEl(null); setAnchorEl(null);
@ -104,9 +119,12 @@ export const ProjectCard = ({
e.preventDefault(); e.preventDefault();
setShowDelDialog(true); setShowDelDialog(true);
}} }}
disabled={!canDeleteProject}
> >
<Delete className={classes.icon} /> <Delete className={classes.icon} />
Delete project {id === DEFAULT_PROJECT_ID && !canDeleteProject
? "You can't delete the default project"
: 'Delete project'}
</MenuItem> </MenuItem>
</Menu> </Menu>
</div> </div>
@ -136,9 +154,9 @@ export const ProjectCard = ({
</div> </div>
<Dialogue <Dialogue
open={showDelDialog} open={showDelDialog}
// @ts-expect-error
onClick={onRemoveProject} onClick={onRemoveProject}
onClose={() => { onClose={event => {
event.preventDefault();
setAnchorEl(null); setAnchorEl(null);
setShowDelDialog(false); setShowDelDialog(false);
}} }}

View File

@ -118,12 +118,12 @@ export const ProjectListNew = () => {
className={styles.cardLink} className={styles.cardLink}
> >
<ProjectCard <ProjectCard
onHover={() => handleHover(project?.id)} onHover={() => handleHover(project.id)}
name={project?.name} name={project.name}
memberCount={project?.memberCount ?? 0} memberCount={project.memberCount ?? 0}
health={project?.health} health={project.health}
id={project?.id} id={project.id}
featureCount={project?.featureCount} featureCount={project.featureCount}
/> />
</Link> </Link>
); );

View File

@ -11,6 +11,7 @@ export const UPDATE_CONTEXT_FIELD = 'UPDATE_CONTEXT_FIELD';
export const DELETE_CONTEXT_FIELD = 'DELETE_CONTEXT_FIELD'; export const DELETE_CONTEXT_FIELD = 'DELETE_CONTEXT_FIELD';
export const CREATE_PROJECT = 'CREATE_PROJECT'; export const CREATE_PROJECT = 'CREATE_PROJECT';
export const UPDATE_PROJECT = 'UPDATE_PROJECT'; export const UPDATE_PROJECT = 'UPDATE_PROJECT';
export const DELETE_PROJECT = 'DELETE_PROJECT';
export const DELETE_TAG_TYPE = 'DELETE_TAG_TYPE'; export const DELETE_TAG_TYPE = 'DELETE_TAG_TYPE';
export const UPDATE_TAG_TYPE = 'UPDATE_TAG_TYPE'; export const UPDATE_TAG_TYPE = 'UPDATE_TAG_TYPE';
export const CREATE_ADDON = 'CREATE_ADDON'; export const CREATE_ADDON = 'CREATE_ADDON';

View File

@ -1,7 +1,8 @@
import useProjects from 'hooks/api/getters/useProjects/useProjects'; import useProjects from 'hooks/api/getters/useProjects/useProjects';
const DEFAULT_PROJECT_ID = 'default'; export const DEFAULT_PROJECT_ID = 'default';
// Get the default project ID, or the first ID if there is no default project.
export const useDefaultProjectId = (): string | undefined => { export const useDefaultProjectId = (): string | undefined => {
const { projects = [] } = useProjects(); const { projects = [] } = useProjects();

View File

@ -8,6 +8,7 @@ export interface IRoute {
parent?: string; parent?: string;
flag?: string; flag?: string;
hidden?: boolean; hidden?: boolean;
enterprise?: boolean;
component: VoidFunctionComponent; component: VoidFunctionComponent;
menu: IRouteMenu; menu: IRouteMenu;
} }

View File

@ -265,5 +265,14 @@ export default createTheme({
}, },
}, },
}, },
MuiMenuItem: {
styleOverrides: {
root: {
'&.Mui-disabled': {
opacity: 0.66,
},
},
},
},
}, },
}); });