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:
commit
bd4651b9ca
@ -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}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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`)
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
|
@ -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);
|
||||||
}}
|
}}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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';
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -265,5 +265,14 @@ export default createTheme({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
MuiMenuItem: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
'&.Mui-disabled': {
|
||||||
|
opacity: 0.66,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user