1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-12 13:48:35 +02:00

fix: yarn lint:fix (#4917)

## About the changes
Running yarn lint:fix solves errors in frontend
This commit is contained in:
Gastón Fournier 2023-10-04 11:28:05 +02:00 committed by GitHub
parent c1f8929ddf
commit bd8b54b5bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 655 additions and 654 deletions

View File

@ -1,57 +1,57 @@
import { render, screen, waitFor } from "@testing-library/react";
import { MemoryRouter, Routes, Route } from "react-router-dom";
import { FeatureView } from "../feature/FeatureView/FeatureView";
import { ThemeProvider } from "themes/ThemeProvider";
import { AccessProvider } from "../providers/AccessProvider/AccessProvider";
import { AnnouncerProvider } from "../common/Announcer/AnnouncerProvider/AnnouncerProvider";
import { testServerRoute, testServerSetup } from "../../utils/testServer";
import { UIProviderContainer } from "../providers/UIProvider/UIProviderContainer";
import { FC } from "react";
import { IPermission } from "../../interfaces/user";
import { SWRConfig } from "swr";
import { ProjectMode } from "../project/Project/hooks/useProjectEnterpriseSettingsForm";
import { render, screen, waitFor } from '@testing-library/react';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
import { FeatureView } from '../feature/FeatureView/FeatureView';
import { ThemeProvider } from 'themes/ThemeProvider';
import { AccessProvider } from '../providers/AccessProvider/AccessProvider';
import { AnnouncerProvider } from '../common/Announcer/AnnouncerProvider/AnnouncerProvider';
import { testServerRoute, testServerSetup } from '../../utils/testServer';
import { UIProviderContainer } from '../providers/UIProvider/UIProviderContainer';
import { FC } from 'react';
import { IPermission } from '../../interfaces/user';
import { SWRConfig } from 'swr';
import { ProjectMode } from '../project/Project/hooks/useProjectEnterpriseSettingsForm';
const server = testServerSetup();
const projectWithCollaborationMode = (mode: ProjectMode) =>
testServerRoute(server, "/api/admin/projects/default", { mode });
testServerRoute(server, '/api/admin/projects/default', { mode });
const changeRequestsEnabledIn = (
env: "development" | "production" | "custom"
env: 'development' | 'production' | 'custom',
) =>
testServerRoute(
server,
"/api/admin/projects/default/change-requests/config",
'/api/admin/projects/default/change-requests/config',
[
{
environment: "development",
type: "development",
environment: 'development',
type: 'development',
requiredApprovals: null,
changeRequestEnabled: env === "development",
changeRequestEnabled: env === 'development',
},
{
environment: "production",
type: "production",
environment: 'production',
type: 'production',
requiredApprovals: 1,
changeRequestEnabled: env === "production",
changeRequestEnabled: env === 'production',
},
{
environment: "custom",
type: "production",
environment: 'custom',
type: 'production',
requiredApprovals: null,
changeRequestEnabled: env === "custom",
changeRequestEnabled: env === 'custom',
},
]
],
);
const uiConfigForEnterprise = () =>
testServerRoute(server, "/api/admin/ui-config", {
environment: "Open Source",
testServerRoute(server, '/api/admin/ui-config', {
environment: 'Open Source',
flags: {
changeRequests: true,
},
versionInfo: {
current: { oss: "4.18.0-beta.5", enterprise: "4.17.0-beta.1" },
current: { oss: '4.18.0-beta.5', enterprise: '4.17.0-beta.1' },
},
disablePasswordAuth: false,
});
@ -59,12 +59,12 @@ const uiConfigForEnterprise = () =>
const setupOtherRoutes = (feature: string) => {
testServerRoute(
server,
"api/admin/projects/default/change-requests/pending",
[]
'api/admin/projects/default/change-requests/pending',
[],
);
testServerRoute(server, `api/admin/client-metrics/features/${feature}`, {
version: 1,
maturity: "stable",
maturity: 'stable',
featureName: feature,
lastHourUsage: [],
seenApplications: [],
@ -86,25 +86,25 @@ const setupOtherRoutes = (feature: string) => {
version: 1,
strategies: [
{
displayName: "Standard",
name: "default",
displayName: 'Standard',
name: 'default',
editable: false,
description:
"The standard strategy is strictly on / off for your entire userbase.",
'The standard strategy is strictly on / off for your entire userbase.',
parameters: [],
deprecated: false,
},
{
displayName: "UserIDs",
name: "userWithId",
displayName: 'UserIDs',
name: 'userWithId',
editable: false,
description:
"Enable the feature for a specific set of userIds.",
'Enable the feature for a specific set of userIds.',
parameters: [
{
name: "userIds",
type: "list",
description: "",
name: 'userIds',
type: 'list',
description: '',
required: false,
},
],
@ -115,17 +115,17 @@ const setupOtherRoutes = (feature: string) => {
};
const userHasPermissions = (permissions: Array<IPermission>) => {
testServerRoute(server, "api/admin/user", {
testServerRoute(server, 'api/admin/user', {
user: {
isAPI: false,
id: 2,
name: "Test",
email: "test@getunleash.ai",
name: 'Test',
email: 'test@getunleash.ai',
imageUrl:
"https://gravatar.com/avatar/e55646b526ff342ff8b43721f0cbdd8e?size=42&default=retro",
seenAt: "2022-11-29T08:21:52.581Z",
'https://gravatar.com/avatar/e55646b526ff342ff8b43721f0cbdd8e?size=42&default=retro',
seenAt: '2022-11-29T08:21:52.581Z',
loginAttempts: 0,
createdAt: "2022-11-21T10:10:33.074Z",
createdAt: '2022-11-21T10:10:33.074Z',
},
permissions,
feedback: [],
@ -136,21 +136,21 @@ const userIsMemberOfProjects = (projects: string[]) => {
userHasPermissions(
projects.map((project) => ({
project,
environment: "irrelevant",
permission: "irrelevant",
}))
environment: 'irrelevant',
permission: 'irrelevant',
})),
);
};
const featureEnvironments = (
feature: string,
environments: Array<{ name: string; strategies: Array<string> }>
environments: Array<{ name: string; strategies: Array<string> }>,
) => {
testServerRoute(server, `/api/admin/projects/default/features/${feature}`, {
environments: environments.map((env) => ({
name: env.name,
enabled: false,
type: "production",
type: 'production',
sortOrder: 1,
strategies: env.strategies.map((strategy) => ({
name: strategy,
@ -162,13 +162,13 @@ const featureEnvironments = (
})),
name: feature,
impressionData: false,
description: "",
project: "default",
description: '',
project: 'default',
stale: false,
variants: [],
createdAt: "2022-11-14T08:16:33.338Z",
createdAt: '2022-11-14T08:16:33.338Z',
lastSeenAt: null,
type: "release",
type: 'release',
archived: false,
children: [],
dependencies: [],
@ -199,7 +199,7 @@ const UnleashUiSetup: FC<{ path: string; pathTemplate: string }> = ({
const strategiesAreDisplayed = async (
firstStrategy: string,
secondStrategy: string
secondStrategy: string,
) => {
await screen.findByText(firstStrategy);
await screen.findByText(secondStrategy);
@ -213,10 +213,10 @@ const getDeleteButtons = async () => {
removeMenus.map(async (menu) => {
menu.click();
const removeButton = screen.getAllByTestId(
"STRATEGY_FORM_REMOVE_ID"
'STRATEGY_FORM_REMOVE_ID',
);
deleteButtons.push(...removeButton);
})
}),
);
return deleteButtons;
};
@ -229,12 +229,12 @@ const deleteButtonsActiveInChangeRequestEnv = async () => {
await waitFor(() => {
// production
const productionStrategyDeleteButton = deleteButtons[1];
expect(productionStrategyDeleteButton).not.toHaveClass("Mui-disabled");
expect(productionStrategyDeleteButton).not.toHaveClass('Mui-disabled');
});
await waitFor(() => {
// custom env
const customEnvStrategyDeleteButton = deleteButtons[2];
expect(customEnvStrategyDeleteButton).toHaveClass("Mui-disabled");
expect(customEnvStrategyDeleteButton).toHaveClass('Mui-disabled');
});
};
@ -246,17 +246,17 @@ const deleteButtonsInactiveInChangeRequestEnv = async () => {
await waitFor(() => {
// production
const productionStrategyDeleteButton = deleteButtons[1];
expect(productionStrategyDeleteButton).toHaveClass("Mui-disabled");
expect(productionStrategyDeleteButton).toHaveClass('Mui-disabled');
});
await waitFor(() => {
// custom env
const customEnvStrategyDeleteButton = deleteButtons[2];
expect(customEnvStrategyDeleteButton).toHaveClass("Mui-disabled");
expect(customEnvStrategyDeleteButton).toHaveClass('Mui-disabled');
});
};
const copyButtonsActiveInOtherEnv = async () => {
const copyButtons = screen.getAllByTestId("STRATEGY_FORM_COPY_ID");
const copyButtons = screen.getAllByTestId('STRATEGY_FORM_COPY_ID');
expect(copyButtons.length).toBe(2);
// production
@ -274,92 +274,92 @@ const openEnvironments = async (envNames: string[]) => {
}
};
test("open mode + non-project member can perform basic change request actions", async () => {
const project = "default";
const featureName = "test";
test('open mode + non-project member can perform basic change request actions', async () => {
const project = 'default';
const featureName = 'test';
featureEnvironments(featureName, [
{ name: "development", strategies: [] },
{ name: "production", strategies: ["userWithId"] },
{ name: "custom", strategies: ["default"] },
{ name: 'development', strategies: [] },
{ name: 'production', strategies: ['userWithId'] },
{ name: 'custom', strategies: ['default'] },
]);
userIsMemberOfProjects([]);
changeRequestsEnabledIn("production");
projectWithCollaborationMode("open");
changeRequestsEnabledIn('production');
projectWithCollaborationMode('open');
uiConfigForEnterprise();
setupOtherRoutes(featureName);
render(
<UnleashUiSetup
pathTemplate="/projects/:projectId/features/:featureId/*"
pathTemplate='/projects/:projectId/features/:featureId/*'
path={`/projects/${project}/features/${featureName}`}
>
<FeatureView />
</UnleashUiSetup>
</UnleashUiSetup>,
);
await openEnvironments(["development", "production", "custom"]);
await openEnvironments(['development', 'production', 'custom']);
await strategiesAreDisplayed("UserIDs", "Standard");
await strategiesAreDisplayed('UserIDs', 'Standard');
await deleteButtonsActiveInChangeRequestEnv();
await copyButtonsActiveInOtherEnv();
});
test("protected mode + project member can perform basic change request actions", async () => {
const project = "default";
const featureName = "test";
test('protected mode + project member can perform basic change request actions', async () => {
const project = 'default';
const featureName = 'test';
featureEnvironments(featureName, [
{ name: "development", strategies: [] },
{ name: "production", strategies: ["userWithId"] },
{ name: "custom", strategies: ["default"] },
{ name: 'development', strategies: [] },
{ name: 'production', strategies: ['userWithId'] },
{ name: 'custom', strategies: ['default'] },
]);
userIsMemberOfProjects([project]);
changeRequestsEnabledIn("production");
projectWithCollaborationMode("protected");
changeRequestsEnabledIn('production');
projectWithCollaborationMode('protected');
uiConfigForEnterprise();
setupOtherRoutes(featureName);
render(
<UnleashUiSetup
pathTemplate="/projects/:projectId/features/:featureId/*"
pathTemplate='/projects/:projectId/features/:featureId/*'
path={`/projects/${project}/features/${featureName}`}
>
<FeatureView />
</UnleashUiSetup>
</UnleashUiSetup>,
);
await openEnvironments(["development", "production", "custom"]);
await openEnvironments(['development', 'production', 'custom']);
await strategiesAreDisplayed("UserIDs", "Standard");
await strategiesAreDisplayed('UserIDs', 'Standard');
await deleteButtonsActiveInChangeRequestEnv();
await copyButtonsActiveInOtherEnv();
});
test("protected mode + non-project member cannot perform basic change request actions", async () => {
const project = "default";
const featureName = "test";
test('protected mode + non-project member cannot perform basic change request actions', async () => {
const project = 'default';
const featureName = 'test';
featureEnvironments(featureName, [
{ name: "development", strategies: [] },
{ name: "production", strategies: ["userWithId"] },
{ name: "custom", strategies: ["default"] },
{ name: 'development', strategies: [] },
{ name: 'production', strategies: ['userWithId'] },
{ name: 'custom', strategies: ['default'] },
]);
userIsMemberOfProjects([]);
changeRequestsEnabledIn("production");
projectWithCollaborationMode("protected");
changeRequestsEnabledIn('production');
projectWithCollaborationMode('protected');
uiConfigForEnterprise();
setupOtherRoutes(featureName);
render(
<UnleashUiSetup
pathTemplate="/projects/:projectId/features/:featureId/*"
pathTemplate='/projects/:projectId/features/:featureId/*'
path={`/projects/${project}/features/${featureName}`}
>
<FeatureView />
</UnleashUiSetup>
</UnleashUiSetup>,
);
await openEnvironments(["development", "production", "custom"]);
await openEnvironments(['development', 'production', 'custom']);
await strategiesAreDisplayed("UserIDs", "Standard");
await strategiesAreDisplayed('UserIDs', 'Standard');
await deleteButtonsInactiveInChangeRequestEnv();
await copyButtonsActiveInOtherEnv();
});

View File

@ -1,5 +1,5 @@
import MenuBookIcon from "@mui/icons-material/MenuBook";
import Codebox from "../Codebox/Codebox";
import MenuBookIcon from '@mui/icons-material/MenuBook';
import Codebox from '../Codebox/Codebox';
import {
Collapse,
IconButton,
@ -7,16 +7,16 @@ import {
Tooltip,
Divider,
styled,
} from "@mui/material";
import { FileCopy, Info } from "@mui/icons-material";
import { ConditionallyRender } from "component/common/ConditionallyRender/ConditionallyRender";
import Loader from "../Loader/Loader";
import copy from "copy-to-clipboard";
import useToast from "hooks/useToast";
import React, { ReactNode, useState } from "react";
import { ReactComponent as MobileGuidanceBG } from "assets/img/mobileGuidanceBg.svg";
import { formTemplateSidebarWidth } from "./FormTemplate.styles";
import { relative } from "themes/themeStyles";
} from '@mui/material';
import { FileCopy, Info } from '@mui/icons-material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import Loader from '../Loader/Loader';
import copy from 'copy-to-clipboard';
import useToast from 'hooks/useToast';
import React, { ReactNode, useState } from 'react';
import { ReactComponent as MobileGuidanceBG } from 'assets/img/mobileGuidanceBg.svg';
import { formTemplateSidebarWidth } from './FormTemplate.styles';
import { relative } from 'themes/themeStyles';
interface ICreateProps {
title?: ReactNode;
@ -34,66 +34,66 @@ interface ICreateProps {
compact?: boolean;
}
const StyledContainer = styled("section", {
const StyledContainer = styled('section', {
shouldForwardProp: (prop) =>
!["modal", "compact"].includes(prop.toString()),
!['modal', 'compact'].includes(prop.toString()),
})<{ modal?: boolean; compact?: boolean }>(({ theme, modal, compact }) => ({
minHeight: modal ? "100vh" : compact ? 0 : "80vh",
minHeight: modal ? '100vh' : compact ? 0 : '80vh',
borderRadius: modal ? 0 : theme.spacing(2),
width: "100%",
display: "flex",
margin: "0 auto",
overflow: modal ? "unset" : "hidden",
width: '100%',
display: 'flex',
margin: '0 auto',
overflow: modal ? 'unset' : 'hidden',
[theme.breakpoints.down(1100)]: {
flexDirection: "column",
flexDirection: 'column',
minHeight: 0,
},
}));
const StyledRelativeDiv = styled("div")(({ theme }) => relative);
const StyledRelativeDiv = styled('div')(({ theme }) => relative);
const StyledMain = styled("div")(({ theme }) => ({
display: "flex",
flexDirection: "column",
const StyledMain = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
flexShrink: 1,
width: "100%",
width: '100%',
[theme.breakpoints.down(1100)]: {
width: "100%",
width: '100%',
},
}));
const StyledFormContent = styled("div", {
const StyledFormContent = styled('div', {
shouldForwardProp: (prop) => {
return !["disablePadding", "compactPadding"].includes(prop.toString());
return !['disablePadding', 'compactPadding'].includes(prop.toString());
},
})<{ disablePadding?: boolean; compactPadding?: boolean }>(
({ theme, disablePadding, compactPadding }) => ({
backgroundColor: theme.palette.background.paper,
display: "flex",
flexDirection: "column",
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
padding: disablePadding
? 0
: compactPadding
? theme.spacing(4)
: theme.spacing(6),
[theme.breakpoints.down("lg")]: {
[theme.breakpoints.down('lg')]: {
padding: disablePadding ? 0 : theme.spacing(4),
},
[theme.breakpoints.down(1100)]: {
width: "100%",
width: '100%',
},
[theme.breakpoints.down(500)]: {
padding: disablePadding ? 0 : theme.spacing(4, 2),
},
})
}),
);
const StyledFooter = styled("div")(({ theme }) => ({
const StyledFooter = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
padding: theme.spacing(4, 6),
[theme.breakpoints.down("lg")]: {
[theme.breakpoints.down('lg')]: {
padding: theme.spacing(4),
},
[theme.breakpoints.down(500)]: {
@ -101,9 +101,9 @@ const StyledFooter = styled("div")(({ theme }) => ({
},
}));
const StyledTitle = styled("h1")(({ theme }) => ({
const StyledTitle = styled('h1')(({ theme }) => ({
marginBottom: theme.fontSizes.mainHeader,
fontWeight: "normal",
fontWeight: 'normal',
}));
const StyledSidebarDivider = styled(Divider)(({ theme }) => ({
@ -111,12 +111,12 @@ const StyledSidebarDivider = styled(Divider)(({ theme }) => ({
marginBottom: theme.spacing(0.5),
}));
const StyledSubtitle = styled("h2")(({ theme }) => ({
const StyledSubtitle = styled('h2')(({ theme }) => ({
color: theme.palette.common.white,
marginBottom: theme.spacing(2),
display: "flex",
justifyContent: "space-between",
alignItems: "center",
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
fontWeight: theme.fontWeight.bold,
fontSize: theme.fontSizes.bodySize,
}));
@ -125,20 +125,20 @@ const StyledIcon = styled(FileCopy)(({ theme }) => ({
fill: theme.palette.primary.contrastText,
}));
const StyledMobileGuidanceContainer = styled("div")(() => ({
const StyledMobileGuidanceContainer = styled('div')(() => ({
zIndex: 1,
position: "absolute",
position: 'absolute',
right: -3,
top: -3,
}));
const StyledMobileGuidanceBackground = styled(MobileGuidanceBG)(() => ({
width: "75px",
height: "75px",
width: '75px',
height: '75px',
}));
const StyledMobileGuidanceButton = styled(IconButton)(() => ({
position: "absolute",
position: 'absolute',
zIndex: 400,
right: 0,
}));
@ -147,31 +147,31 @@ const StyledInfoIcon = styled(Info)(({ theme }) => ({
fill: theme.palette.primary.contrastText,
}));
const StyledSidebar = styled("aside")(({ theme }) => ({
const StyledSidebar = styled('aside')(({ theme }) => ({
backgroundColor: theme.palette.background.sidebar,
padding: theme.spacing(4),
flexGrow: 0,
flexShrink: 0,
width: formTemplateSidebarWidth,
[theme.breakpoints.down(1100)]: {
width: "100%",
color: "red",
width: '100%',
color: 'red',
},
[theme.breakpoints.down(500)]: {
padding: theme.spacing(4, 2),
},
}));
const StyledDescription = styled("p")(({ theme }) => ({
const StyledDescription = styled('p')(({ theme }) => ({
color: theme.palette.common.white,
zIndex: 1,
position: "relative",
position: 'relative',
}));
const StyledLinkContainer = styled("div")(({ theme }) => ({
const StyledLinkContainer = styled('div')(({ theme }) => ({
margin: theme.spacing(3, 0),
display: "flex",
alignItems: "center",
display: 'flex',
alignItems: 'center',
}));
const StyledLinkIcon = styled(MenuBookIcon)(({ theme }) => ({
@ -179,11 +179,11 @@ const StyledLinkIcon = styled(MenuBookIcon)(({ theme }) => ({
color: theme.palette.primary.contrastText,
}));
const StyledDocumentationLink = styled("a")(({ theme }) => ({
const StyledDocumentationLink = styled('a')(({ theme }) => ({
color: theme.palette.primary.contrastText,
display: "block",
"&:hover": {
textDecoration: "none",
display: 'block',
'&:hover': {
textDecoration: 'none',
},
}));
@ -209,18 +209,18 @@ const FormTemplate: React.FC<ICreateProps> = ({
if (formatApiCode !== undefined) {
if (copy(formatApiCode())) {
setToastData({
title: "Successfully copied the command",
text: "The command should now be automatically copied to your clipboard",
title: 'Successfully copied the command',
text: 'The command should now be automatically copied to your clipboard',
autoHideDuration: 6000,
type: "success",
type: 'success',
show: true,
});
} else {
setToastData({
title: "Could not copy the command",
text: "Sorry, but we could not copy the command.",
title: 'Could not copy the command',
text: 'Sorry, but we could not copy the command.',
autoHideDuration: 6000,
type: "error",
type: 'error',
show: true,
});
}
@ -236,14 +236,14 @@ const FormTemplate: React.FC<ICreateProps> = ({
show={<StyledSidebarDivider />}
/>
<StyledSubtitle>
API Command{" "}
<Tooltip title="Copy command" arrow>
<IconButton onClick={copyCommand} size="large">
API Command{' '}
<Tooltip title='Copy command' arrow>
<IconButton onClick={copyCommand} size='large'>
<StyledIcon />
</IconButton>
</Tooltip>
</StyledSubtitle>
<Codebox text={formatApiCode!()} />{" "}
<Codebox text={formatApiCode!()} />{' '}
</>
);
}
@ -304,7 +304,7 @@ const FormTemplate: React.FC<ICreateProps> = ({
>
{renderApiInfo(
formatApiCode === undefined,
!(showDescription || showLink)
!(showDescription || showLink),
)}
</Guidance>
}
@ -331,10 +331,10 @@ const MobileGuidance = ({
<StyledMobileGuidanceContainer>
<StyledMobileGuidanceBackground />
</StyledMobileGuidanceContainer>
<Tooltip title="Toggle help" arrow>
<Tooltip title='Toggle help' arrow>
<StyledMobileGuidanceButton
onClick={() => setOpen((prev) => !prev)}
size="large"
size='large'
>
<StyledInfoIcon />
</StyledMobileGuidanceButton>
@ -362,7 +362,7 @@ const Guidance: React.FC<IGuidanceProps> = ({
description,
children,
documentationLink,
documentationLinkLabel = "Learn more",
documentationLinkLabel = 'Learn more',
showDescription = true,
showLink = true,
}) => {
@ -380,8 +380,8 @@ const Guidance: React.FC<IGuidanceProps> = ({
<StyledLinkIcon />
<StyledDocumentationLink
href={documentationLink}
rel="noopener noreferrer"
target="_blank"
rel='noopener noreferrer'
target='_blank'
>
{documentationLinkLabel}
</StyledDocumentationLink>

View File

@ -1,21 +1,21 @@
import { useNavigate } from "react-router-dom";
import ProjectForm from "../ProjectForm/ProjectForm";
import { useNavigate } from 'react-router-dom';
import ProjectForm from '../ProjectForm/ProjectForm';
import useProjectForm, {
DEFAULT_PROJECT_STICKINESS,
} from "../hooks/useProjectForm";
import { CreateButton } from "component/common/CreateButton/CreateButton";
import FormTemplate from "component/common/FormTemplate/FormTemplate";
import { CREATE_PROJECT } from "component/providers/AccessProvider/permissions";
import useProjectApi from "hooks/api/actions/useProjectApi/useProjectApi";
import { useAuthUser } from "hooks/api/getters/useAuth/useAuthUser";
import useUiConfig from "hooks/api/getters/useUiConfig/useUiConfig";
import useToast from "hooks/useToast";
import { formatUnknownError } from "utils/formatUnknownError";
import { GO_BACK } from "constants/navigate";
import { usePlausibleTracker } from "hooks/usePlausibleTracker";
import { Button, styled } from "@mui/material";
} from '../hooks/useProjectForm';
import { CreateButton } from 'component/common/CreateButton/CreateButton';
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { CREATE_PROJECT } from 'component/providers/AccessProvider/permissions';
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { GO_BACK } from 'constants/navigate';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { Button, styled } from '@mui/material';
const CREATE_PROJECT_BTN = "CREATE_PROJECT_BTN";
const CREATE_PROJECT_BTN = 'CREATE_PROJECT_BTN';
const StyledButton = styled(Button)(({ theme }) => ({
marginLeft: theme.spacing(3),
@ -60,17 +60,17 @@ const CreateProject = () => {
refetchUser();
navigate(`/projects/${projectId}`);
setToastData({
title: "Project created",
text: "Now you can add toggles to this project",
title: 'Project created',
text: 'Now you can add toggles to this project',
confetti: true,
type: "success",
type: 'success',
});
if (projectStickiness !== DEFAULT_PROJECT_STICKINESS) {
trackEvent("project_stickiness_set");
trackEvent('project_stickiness_set');
}
trackEvent("project-mode", {
props: { mode: projectMode, action: "added" },
trackEvent('project-mode', {
props: { mode: projectMode, action: 'added' },
});
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
@ -79,9 +79,7 @@ const CreateProject = () => {
};
const formatApiCode = () => {
return `curl --location --request POST '${
uiConfig.unleashUrl
}/api/admin/projects' \\
return `curl --location --request POST '${uiConfig.unleashUrl}/api/admin/projects' \\
--header 'Authorization: INSERT_API_KEY' \\
--header 'Content-Type: application/json' \\
--data-raw '${JSON.stringify(getCreateProjectPayload(), undefined, 2)}'`;
@ -94,10 +92,10 @@ const CreateProject = () => {
return (
<FormTemplate
loading={loading}
title="Create project"
description="Projects allows you to group feature toggles together in the management UI."
documentationLink="https://docs.getunleash.io/reference/projects"
documentationLinkLabel="Projects documentation"
title='Create project'
description='Projects allows you to group feature toggles together in the management UI.'
documentationLink='https://docs.getunleash.io/reference/projects'
documentationLinkLabel='Projects documentation'
formatApiCode={formatApiCode}
>
<ProjectForm
@ -113,12 +111,12 @@ const CreateProject = () => {
setProjectName={setProjectName}
projectDesc={projectDesc}
setProjectDesc={setProjectDesc}
mode="Create"
mode='Create'
clearErrors={clearErrors}
validateProjectId={validateProjectId}
>
<CreateButton
name="project"
name='project'
permission={CREATE_PROJECT}
data-testid={CREATE_PROJECT_BTN}
/>

View File

@ -1,20 +1,20 @@
import { Box, styled, Typography } from "@mui/material";
import { FC } from "react";
import { HelpIcon } from "component/common/HelpIcon/HelpIcon";
import { useUiFlag } from "hooks/useUiFlag";
import { ConditionallyRender } from "component/common/ConditionallyRender/ConditionallyRender";
import { Box, styled, Typography } from '@mui/material';
import { FC } from 'react';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
import { useUiFlag } from 'hooks/useUiFlag';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
const StyledTitle = styled(Typography)(({ theme }) => ({
fontWeight: theme.fontWeight.bold,
display: "inline",
display: 'inline',
}));
const StyledDescription = styled(Typography)(({ theme }) => ({
display: "inline",
display: 'inline',
color: theme.palette.text.secondary,
}));
export const CollaborationModeTooltip: FC = () => {
const privateProjects = useUiFlag("privateProjects");
const privateProjects = useUiFlag('privateProjects');
return (
<HelpIcon
htmlTooltip

View File

@ -1,6 +1,6 @@
import { Box } from "@mui/material";
import { FC } from "react";
import { HelpIcon } from "component/common/HelpIcon/HelpIcon";
import { Box } from '@mui/material';
import { FC } from 'react';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
export const FeatureFlagNamingTooltip: FC = () => {
return (
@ -9,8 +9,8 @@ export const FeatureFlagNamingTooltip: FC = () => {
tooltip={
<Box>
<p>
For example, the pattern{" "}
<code>{"[a-z0-9]{2}\\.[a-z]{4,12}"}</code> matches
For example, the pattern{' '}
<code>{'[a-z0-9]{2}\\.[a-z]{4,12}'}</code> matches
'a1.project', but not 'a1.project.feature-1'.
</p>
</Box>

View File

@ -1,13 +1,13 @@
import React, { useEffect } from "react";
import { ConditionallyRender } from "component/common/ConditionallyRender/ConditionallyRender";
import Select from "component/common/select";
import { ProjectMode } from "../hooks/useProjectEnterpriseSettingsForm";
import { Box, InputAdornment, styled, TextField } from "@mui/material";
import { CollaborationModeTooltip } from "./CollaborationModeTooltip";
import Input from "component/common/Input/Input";
import { FeatureFlagNamingTooltip } from "./FeatureFlagNamingTooltip";
import { usePlausibleTracker } from "hooks/usePlausibleTracker";
import { useUiFlag } from "hooks/useUiFlag";
import React, { useEffect } from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import Select from 'component/common/select';
import { ProjectMode } from '../hooks/useProjectEnterpriseSettingsForm';
import { Box, InputAdornment, styled, TextField } from '@mui/material';
import { CollaborationModeTooltip } from './CollaborationModeTooltip';
import Input from 'component/common/Input/Input';
import { FeatureFlagNamingTooltip } from './FeatureFlagNamingTooltip';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { useUiFlag } from 'hooks/useUiFlag';
interface IProjectEnterpriseSettingsForm {
projectId: string;
@ -24,12 +24,12 @@ interface IProjectEnterpriseSettingsForm {
clearErrors: () => void;
}
const StyledForm = styled("form")(({ theme }) => ({
height: "100%",
const StyledForm = styled('form')(({ theme }) => ({
height: '100%',
paddingBottom: theme.spacing(4),
}));
const StyledSubtitle = styled("div")(({ theme }) => ({
const StyledSubtitle = styled('div')(({ theme }) => ({
color: theme.palette.text.secondary,
fontSize: theme.fontSizes.smallerBody,
lineHeight: 1.25,
@ -37,42 +37,42 @@ const StyledSubtitle = styled("div")(({ theme }) => ({
}));
const StyledInput = styled(Input)(({ theme }) => ({
width: "100%",
width: '100%',
marginBottom: theme.spacing(2),
paddingRight: theme.spacing(1),
}));
const StyledTextField = styled(TextField)(({ theme }) => ({
width: "100%",
width: '100%',
marginBottom: theme.spacing(2),
}));
const StyledFieldset = styled("fieldset")(() => ({
const StyledFieldset = styled('fieldset')(() => ({
padding: 0,
border: "none",
border: 'none',
}));
const StyledSelect = styled(Select)(({ theme }) => ({
marginBottom: theme.spacing(2),
minWidth: "200px",
minWidth: '200px',
}));
const StyledButtonContainer = styled("div")(() => ({
marginTop: "auto",
display: "flex",
justifyContent: "flex-end",
const StyledButtonContainer = styled('div')(() => ({
marginTop: 'auto',
display: 'flex',
justifyContent: 'flex-end',
}));
const StyledFlagNamingContainer = styled("div")(({ theme }) => ({
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
const StyledFlagNamingContainer = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
gap: theme.spacing(1),
"& > *": { width: "100%" },
'& > *': { width: '100%' },
}));
const StyledPatternNamingExplanation = styled("div")(({ theme }) => ({
"p + p": { marginTop: theme.spacing(1) },
const StyledPatternNamingExplanation = styled('div')(({ theme }) => ({
'p + p': { marginTop: theme.spacing(1) },
}));
export const validateFeatureNamingExample = ({
@ -83,285 +83,288 @@ export const validateFeatureNamingExample = ({
pattern: string;
example: string;
featureNamingPatternError: string | undefined;
}): { state: "valid" } | { state: "invalid"; reason: string } => {
}): { state: 'valid' } | { state: 'invalid'; reason: string } => {
if (featureNamingPatternError || !example || !pattern) {
return { state: "valid" };
return { state: 'valid' };
} else if (example && pattern) {
const regex = new RegExp(`^${pattern}$`);
const matches = regex.test(example);
if (!matches) {
return { state: "invalid", reason: "Example does not match regex" };
return { state: 'invalid', reason: 'Example does not match regex' };
} else {
return { state: "valid" };
return { state: 'valid' };
}
}
return { state: "valid" };
return { state: 'valid' };
};
const useFeatureNamePatternTracking = () => {
const [previousPattern, setPreviousPattern] = React.useState<string>("");
const [previousPattern, setPreviousPattern] = React.useState<string>('');
const { trackEvent } = usePlausibleTracker();
const eventName = "feature-naming-pattern" as const;
const eventName = 'feature-naming-pattern' as const;
const trackPattern = (pattern: string = "") => {
const trackPattern = (pattern: string = '') => {
if (pattern === previousPattern) {
// do nothing; they've probably updated something else in the
// project.
} else if (pattern === "" && previousPattern !== "") {
trackEvent(eventName, { props: { action: "removed" } });
} else if (pattern !== "" && previousPattern === "") {
trackEvent(eventName, { props: { action: "added" } });
} else if (pattern !== "" && previousPattern !== "") {
trackEvent(eventName, { props: { action: "edited" } });
} else if (pattern === '' && previousPattern !== '') {
trackEvent(eventName, { props: { action: 'removed' } });
} else if (pattern !== '' && previousPattern === '') {
trackEvent(eventName, { props: { action: 'added' } });
} else if (pattern !== '' && previousPattern !== '') {
trackEvent(eventName, { props: { action: 'edited' } });
}
};
return { trackPattern, setPreviousPattern };
};
const ProjectEnterpriseSettingsForm: React.FC<
IProjectEnterpriseSettingsForm
> = ({
children,
handleSubmit,
projectId,
projectMode,
featureNamingExample,
featureNamingPattern,
featureNamingDescription,
setFeatureNamingExample,
setFeatureNamingPattern,
setFeatureNamingDescription,
setProjectMode,
errors,
clearErrors,
}) => {
const privateProjects = useUiFlag("privateProjects");
const shouldShowFlagNaming = useUiFlag("featureNamingPattern");
const { setPreviousPattern, trackPattern } =
useFeatureNamePatternTracking();
const projectModeOptions = privateProjects
? [
{ key: "open", label: "open" },
{ key: "protected", label: "protected" },
{ key: "private", label: "private" },
]
: [
{ key: "open", label: "open" },
{ key: "protected", label: "protected" },
];
useEffect(() => {
setPreviousPattern(featureNamingPattern || "");
}, [projectId]);
const updateNamingExampleError = ({
example,
pattern,
}: {
example: string;
pattern: string;
const ProjectEnterpriseSettingsForm: React.FC<IProjectEnterpriseSettingsForm> =
({
children,
handleSubmit,
projectId,
projectMode,
featureNamingExample,
featureNamingPattern,
featureNamingDescription,
setFeatureNamingExample,
setFeatureNamingPattern,
setFeatureNamingDescription,
setProjectMode,
errors,
clearErrors,
}) => {
const validationResult = validateFeatureNamingExample({
const privateProjects = useUiFlag('privateProjects');
const shouldShowFlagNaming = useUiFlag('featureNamingPattern');
const { setPreviousPattern, trackPattern } =
useFeatureNamePatternTracking();
const projectModeOptions = privateProjects
? [
{ key: 'open', label: 'open' },
{ key: 'protected', label: 'protected' },
{ key: 'private', label: 'private' },
]
: [
{ key: 'open', label: 'open' },
{ key: 'protected', label: 'protected' },
];
useEffect(() => {
setPreviousPattern(featureNamingPattern || '');
}, [projectId]);
const updateNamingExampleError = ({
example,
pattern,
example,
featureNamingPatternError: errors.featureNamingPattern,
});
}: {
example: string;
pattern: string;
}) => {
const validationResult = validateFeatureNamingExample({
pattern,
example,
featureNamingPatternError: errors.featureNamingPattern,
});
switch (validationResult.state) {
case "invalid":
errors.namingExample = validationResult.reason;
break;
case "valid":
delete errors.namingExample;
break;
}
};
const onSetFeatureNamingPattern = (regex: string) => {
const disallowedStrings = [
" ",
"\\t",
"\\s",
"\\n",
"\\r",
"\\f",
"\\v",
];
if (
disallowedStrings.some((blockedString) =>
regex.includes(blockedString)
)
) {
errors.featureNamingPattern =
"Whitespace is not allowed in the expression";
} else {
try {
new RegExp(regex);
delete errors.featureNamingPattern;
} catch (e) {
errors.featureNamingPattern = "Invalid regular expression";
switch (validationResult.state) {
case 'invalid':
errors.namingExample = validationResult.reason;
break;
case 'valid':
delete errors.namingExample;
break;
}
}
setFeatureNamingPattern?.(regex);
updateNamingExampleError({
pattern: regex,
example: featureNamingExample || "",
});
};
};
const onSetFeatureNamingExample = (example: string) => {
setFeatureNamingExample && setFeatureNamingExample(example);
updateNamingExampleError({
pattern: featureNamingPattern || "",
example,
});
};
const onSetFeatureNamingPattern = (regex: string) => {
const disallowedStrings = [
' ',
'\\t',
'\\s',
'\\n',
'\\r',
'\\f',
'\\v',
];
if (
disallowedStrings.some((blockedString) =>
regex.includes(blockedString),
)
) {
errors.featureNamingPattern =
'Whitespace is not allowed in the expression';
} else {
try {
new RegExp(regex);
delete errors.featureNamingPattern;
} catch (e) {
errors.featureNamingPattern = 'Invalid regular expression';
}
}
setFeatureNamingPattern?.(regex);
updateNamingExampleError({
pattern: regex,
example: featureNamingExample || '',
});
};
const onSetFeatureNamingDescription = (description: string) => {
setFeatureNamingDescription?.(description);
};
const onSetFeatureNamingExample = (example: string) => {
setFeatureNamingExample && setFeatureNamingExample(example);
updateNamingExampleError({
pattern: featureNamingPattern || '',
example,
});
};
return (
<StyledForm
onSubmit={(submitEvent) => {
handleSubmit(submitEvent);
trackPattern(featureNamingPattern);
}}
>
<>
<Box
sx={{
display: "flex",
alignItems: "center",
marginBottom: 1,
gap: 1,
}}
>
<p>What is your project collaboration mode?</p>
<CollaborationModeTooltip />
</Box>
<StyledSelect
id="project-mode"
value={projectMode}
label="Project collaboration mode"
name="Project collaboration mode"
onChange={(e) => {
setProjectMode?.(e.target.value as ProjectMode);
}}
options={projectModeOptions}
/>
</>
<ConditionallyRender
condition={Boolean(shouldShowFlagNaming)}
show={
<StyledFieldset>
<Box
sx={{
display: "flex",
alignItems: "center",
marginBottom: 1,
gap: 1,
}}
>
<legend>Feature flag naming pattern?</legend>
<FeatureFlagNamingTooltip />
</Box>
<StyledSubtitle>
<StyledPatternNamingExplanation id="pattern-naming-description">
<p>
Define a{" "}
<a
href={`https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions/Cheatsheet`}
target="_blank"
rel="noreferrer"
>
JavaScript RegEx
</a>{" "}
used to enforce feature flag names within
this project. The regex will be surrounded
by a leading <code>^</code> and a trailing{" "}
<code>$</code>.
</p>
<p>
Leave it empty if you dont want to add a
naming pattern.
</p>
</StyledPatternNamingExplanation>
</StyledSubtitle>
<StyledFlagNamingContainer>
<StyledInput
label={"Naming Pattern"}
name="feature flag naming pattern"
aria-describedby="pattern-naming-description"
placeholder="[A-Za-z]+\.[A-Za-z]+\.[A-Za-z0-9-]+"
InputProps={{
startAdornment: (
<InputAdornment position="start">
^
</InputAdornment>
),
endAdornment: (
<InputAdornment position="end">
$
</InputAdornment>
),
const onSetFeatureNamingDescription = (description: string) => {
setFeatureNamingDescription?.(description);
};
return (
<StyledForm
onSubmit={(submitEvent) => {
handleSubmit(submitEvent);
trackPattern(featureNamingPattern);
}}
>
<>
<Box
sx={{
display: 'flex',
alignItems: 'center',
marginBottom: 1,
gap: 1,
}}
>
<p>What is your project collaboration mode?</p>
<CollaborationModeTooltip />
</Box>
<StyledSelect
id='project-mode'
value={projectMode}
label='Project collaboration mode'
name='Project collaboration mode'
onChange={(e) => {
setProjectMode?.(e.target.value as ProjectMode);
}}
options={projectModeOptions}
/>
</>
<ConditionallyRender
condition={Boolean(shouldShowFlagNaming)}
show={
<StyledFieldset>
<Box
sx={{
display: 'flex',
alignItems: 'center',
marginBottom: 1,
gap: 1,
}}
type={"text"}
value={featureNamingPattern || ""}
error={Boolean(errors.featureNamingPattern)}
errorText={errors.featureNamingPattern}
onChange={(e) =>
onSetFeatureNamingPattern(e.target.value)
}
/>
>
<legend>Feature flag naming pattern?</legend>
<FeatureFlagNamingTooltip />
</Box>
<StyledSubtitle>
<p id="pattern-additional-description">
The example and description will be shown to
users when they create a new feature flag in
this project.
</p>
<StyledPatternNamingExplanation id='pattern-naming-description'>
<p>
Define a{' '}
<a
href={`https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions/Cheatsheet`}
target='_blank'
rel='noreferrer'
>
JavaScript RegEx
</a>{' '}
used to enforce feature flag names
within this project. The regex will be
surrounded by a leading <code>^</code>{' '}
and a trailing <code>$</code>.
</p>
<p>
Leave it empty if you dont want to add
a naming pattern.
</p>
</StyledPatternNamingExplanation>
</StyledSubtitle>
<StyledFlagNamingContainer>
<StyledInput
label={'Naming Pattern'}
name='feature flag naming pattern'
aria-describedby='pattern-naming-description'
placeholder='[A-Za-z]+.[A-Za-z]+.[A-Za-z0-9-]+'
InputProps={{
startAdornment: (
<InputAdornment position='start'>
^
</InputAdornment>
),
endAdornment: (
<InputAdornment position='end'>
$
</InputAdornment>
),
}}
type={'text'}
value={featureNamingPattern || ''}
error={Boolean(errors.featureNamingPattern)}
errorText={errors.featureNamingPattern}
onChange={(e) =>
onSetFeatureNamingPattern(
e.target.value,
)
}
/>
<StyledSubtitle>
<p id='pattern-additional-description'>
The example and description will be
shown to users when they create a new
feature flag in this project.
</p>
</StyledSubtitle>
<StyledInput
label={"Naming Example"}
name="feature flag naming example"
type={"text"}
aria-describedby="pattern-additional-description"
value={featureNamingExample || ""}
placeholder="dx.feature1.1-135"
error={Boolean(errors.namingExample)}
errorText={errors.namingExample}
onChange={(e) =>
onSetFeatureNamingExample(e.target.value)
}
/>
<StyledTextField
label={"Naming pattern description"}
name="feature flag naming description"
type={"text"}
aria-describedby="pattern-additional-description"
placeholder={`<project>.<featureName>.<ticket>
<StyledInput
label={'Naming Example'}
name='feature flag naming example'
type={'text'}
aria-describedby='pattern-additional-description'
value={featureNamingExample || ''}
placeholder='dx.feature1.1-135'
error={Boolean(errors.namingExample)}
errorText={errors.namingExample}
onChange={(e) =>
onSetFeatureNamingExample(
e.target.value,
)
}
/>
<StyledTextField
label={'Naming pattern description'}
name='feature flag naming description'
type={'text'}
aria-describedby='pattern-additional-description'
placeholder={`<project>.<featureName>.<ticket>
The flag name should contain the project name, the feature name, and the ticket number, each separated by a dot.`}
multiline
minRows={5}
value={featureNamingDescription || ""}
onChange={(e) =>
onSetFeatureNamingDescription(
e.target.value
)
}
/>
</StyledFlagNamingContainer>
</StyledFieldset>
}
/>
<StyledButtonContainer>{children}</StyledButtonContainer>
</StyledForm>
);
};
multiline
minRows={5}
value={featureNamingDescription || ''}
onChange={(e) =>
onSetFeatureNamingDescription(
e.target.value,
)
}
/>
</StyledFlagNamingContainer>
</StyledFieldset>
}
/>
<StyledButtonContainer>{children}</StyledButtonContainer>
</StyledForm>
);
};
export default ProjectEnterpriseSettingsForm;

View File

@ -1,15 +1,15 @@
import React from "react";
import { trim } from "component/common/util";
import { StickinessSelect } from "component/feature/StrategyTypes/FlexibleStrategy/StickinessSelect/StickinessSelect";
import { ConditionallyRender } from "component/common/ConditionallyRender/ConditionallyRender";
import { Box, styled, TextField } from "@mui/material";
import Input from "component/common/Input/Input";
import { FeatureTogglesLimitTooltip } from "./FeatureTogglesLimitTooltip";
import { ProjectMode } from "../hooks/useProjectEnterpriseSettingsForm";
import useUiConfig from "hooks/api/getters/useUiConfig/useUiConfig";
import { CollaborationModeTooltip } from "../ProjectEnterpriseSettingsForm/CollaborationModeTooltip";
import Select from "component/common/select";
import { useUiFlag } from "hooks/useUiFlag";
import React from 'react';
import { trim } from 'component/common/util';
import { StickinessSelect } from 'component/feature/StrategyTypes/FlexibleStrategy/StickinessSelect/StickinessSelect';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Box, styled, TextField } from '@mui/material';
import Input from 'component/common/Input/Input';
import { FeatureTogglesLimitTooltip } from './FeatureTogglesLimitTooltip';
import { ProjectMode } from '../hooks/useProjectEnterpriseSettingsForm';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { CollaborationModeTooltip } from '../ProjectEnterpriseSettingsForm/CollaborationModeTooltip';
import Select from 'component/common/select';
import { useUiFlag } from 'hooks/useUiFlag';
interface IProjectForm {
projectId: string;
@ -27,32 +27,32 @@ interface IProjectForm {
setProjectMode?: React.Dispatch<React.SetStateAction<ProjectMode>>;
handleSubmit: (e: any) => void;
errors: { [key: string]: string };
mode: "Create" | "Edit";
mode: 'Create' | 'Edit';
clearErrors: () => void;
validateProjectId: () => void;
}
const PROJECT_STICKINESS_SELECT = "PROJECT_STICKINESS_SELECT";
const PROJECT_ID_INPUT = "PROJECT_ID_INPUT";
const PROJECT_NAME_INPUT = "PROJECT_NAME_INPUT";
const PROJECT_DESCRIPTION_INPUT = "PROJECT_DESCRIPTION_INPUT";
const PROJECT_STICKINESS_SELECT = 'PROJECT_STICKINESS_SELECT';
const PROJECT_ID_INPUT = 'PROJECT_ID_INPUT';
const PROJECT_NAME_INPUT = 'PROJECT_NAME_INPUT';
const PROJECT_DESCRIPTION_INPUT = 'PROJECT_DESCRIPTION_INPUT';
const StyledForm = styled("form")(({ theme }) => ({
height: "100%",
const StyledForm = styled('form')(({ theme }) => ({
height: '100%',
paddingBottom: theme.spacing(1),
}));
const StyledDescription = styled("p")(({ theme }) => ({
const StyledDescription = styled('p')(({ theme }) => ({
marginBottom: theme.spacing(1),
marginRight: theme.spacing(1),
}));
const StyledSelect = styled(Select)(({ theme }) => ({
marginBottom: theme.spacing(2),
minWidth: "200px",
minWidth: '200px',
}));
const StyledSubtitle = styled("div")(({ theme }) => ({
const StyledSubtitle = styled('div')(({ theme }) => ({
color: theme.palette.text.secondary,
fontSize: theme.fontSizes.smallerBody,
lineHeight: 1.25,
@ -60,25 +60,25 @@ const StyledSubtitle = styled("div")(({ theme }) => ({
}));
const StyledInput = styled(Input)(({ theme }) => ({
width: "100%",
width: '100%',
marginBottom: theme.spacing(2),
paddingRight: theme.spacing(1),
}));
const StyledTextField = styled(TextField)(({ theme }) => ({
width: "100%",
width: '100%',
marginBottom: theme.spacing(2),
}));
const StyledButtonContainer = styled("div")(() => ({
marginTop: "auto",
display: "flex",
justifyContent: "flex-end",
const StyledButtonContainer = styled('div')(() => ({
marginTop: 'auto',
display: 'flex',
justifyContent: 'flex-end',
}));
const StyledInputContainer = styled("div")(() => ({
display: "flex",
alignItems: "center",
const StyledInputContainer = styled('div')(() => ({
display: 'flex',
alignItems: 'center',
}));
const ProjectForm: React.FC<IProjectForm> = ({
@ -103,17 +103,17 @@ const ProjectForm: React.FC<IProjectForm> = ({
clearErrors,
}) => {
const { isEnterprise } = useUiConfig();
const privateProjects = useUiFlag("privateProjects");
const privateProjects = useUiFlag('privateProjects');
const projectModeOptions = privateProjects
? [
{ key: "open", label: "open" },
{ key: "protected", label: "protected" },
{ key: "private", label: "private" },
{ key: 'open', label: 'open' },
{ key: 'protected', label: 'protected' },
{ key: 'private', label: 'private' },
]
: [
{ key: "open", label: "open" },
{ key: "protected", label: "protected" },
{ key: 'open', label: 'open' },
{ key: 'protected', label: 'protected' },
];
return (
@ -124,14 +124,14 @@ const ProjectForm: React.FC<IProjectForm> = ({
>
<StyledDescription>What is your project Id?</StyledDescription>
<StyledInput
label="Project Id"
label='Project Id'
value={projectId}
onChange={(e) => setProjectId(trim(e.target.value))}
error={Boolean(errors.id)}
errorText={errors.id}
onFocus={() => clearErrors()}
onBlur={validateProjectId}
disabled={mode === "Edit"}
disabled={mode === 'Edit'}
data-testid={PROJECT_ID_INPUT}
autoFocus
required
@ -139,7 +139,7 @@ const ProjectForm: React.FC<IProjectForm> = ({
<StyledDescription>What is your project name?</StyledDescription>
<StyledInput
label="Project name"
label='Project name'
value={projectName}
onChange={(e) => setProjectName(e.target.value)}
error={Boolean(errors.name)}
@ -155,8 +155,8 @@ const ProjectForm: React.FC<IProjectForm> = ({
What is your project description?
</StyledDescription>
<StyledTextField
label="Project description"
variant="outlined"
label='Project description'
variant='outlined'
multiline
maxRows={4}
value={projectDesc}
@ -172,7 +172,7 @@ const ProjectForm: React.FC<IProjectForm> = ({
What is the default stickiness for the project?
</StyledDescription>
<StickinessSelect
label="Stickiness"
label='Stickiness'
value={projectStickiness}
data-testid={PROJECT_STICKINESS_SELECT}
onChange={(e) =>
@ -184,13 +184,13 @@ const ProjectForm: React.FC<IProjectForm> = ({
}
/>
<ConditionallyRender
condition={mode === "Edit" && Boolean(setFeatureLimit)}
condition={mode === 'Edit' && Boolean(setFeatureLimit)}
show={
<>
<Box
sx={{
display: "flex",
alignItems: "center",
display: 'flex',
alignItems: 'center',
marginBottom: 1,
gap: 1,
}}
@ -204,9 +204,9 @@ const ProjectForm: React.FC<IProjectForm> = ({
<StyledInputContainer>
{featureLimit && setFeatureLimit && (
<StyledInput
label={"Limit"}
name="value"
type={"number"}
label={'Limit'}
name='value'
type={'number'}
value={featureLimit}
onChange={(e) =>
setFeatureLimit(e.target.value)
@ -229,13 +229,13 @@ const ProjectForm: React.FC<IProjectForm> = ({
}
/>
<ConditionallyRender
condition={mode === "Create" && isEnterprise()}
condition={mode === 'Create' && isEnterprise()}
show={
<>
<Box
sx={{
display: "flex",
alignItems: "center",
display: 'flex',
alignItems: 'center',
marginBottom: 1,
gap: 1,
}}
@ -244,10 +244,10 @@ const ProjectForm: React.FC<IProjectForm> = ({
<CollaborationModeTooltip />
</Box>
<StyledSelect
id="project-mode"
id='project-mode'
value={projectMode}
label="Project collaboration mode"
name="Project collaboration mode"
label='Project collaboration mode'
name='Project collaboration mode'
onChange={(e) => {
setProjectMode?.(e.target.value as ProjectMode);
}}

View File

@ -1,15 +1,15 @@
import React from "react";
import { DeleteProject } from "../DeleteProject";
import FormTemplate from "component/common/FormTemplate/FormTemplate";
import { useRequiredPathParam } from "hooks/useRequiredPathParam";
import useProjectApi from "hooks/api/actions/useProjectApi/useProjectApi";
import useUiConfig from "hooks/api/getters/useUiConfig/useUiConfig";
import React from 'react';
import { DeleteProject } from '../DeleteProject';
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
interface IDeleteProjectForm {
featureCount: number;
}
export const DeleteProjectForm = ({ featureCount }: IDeleteProjectForm) => {
const id = useRequiredPathParam("projectId");
const id = useRequiredPathParam('projectId');
const { uiConfig } = useUiConfig();
const { loading } = useProjectApi();
const formatProjectDeleteApiCode = () => {
@ -20,10 +20,10 @@ export const DeleteProjectForm = ({ featureCount }: IDeleteProjectForm) => {
return (
<FormTemplate
loading={loading}
title="Delete Project"
description=""
documentationLink="https://docs.getunleash.io/reference/projects"
documentationLinkLabel="Projects documentation"
title='Delete Project'
description=''
documentationLink='https://docs.getunleash.io/reference/projects'
documentationLinkLabel='Projects documentation'
formatApiCode={formatProjectDeleteApiCode}
compact
compactPadding

View File

@ -1,29 +1,29 @@
import { UPDATE_PROJECT } from "component/providers/AccessProvider/permissions";
import useProject from "hooks/api/getters/useProject/useProject";
import useUiConfig from "hooks/api/getters/useUiConfig/useUiConfig";
import { useRequiredPathParam } from "hooks/useRequiredPathParam";
import React, { useContext } from "react";
import AccessContext from "contexts/AccessContext";
import { Alert, styled } from "@mui/material";
import { ConditionallyRender } from "component/common/ConditionallyRender/ConditionallyRender";
import { UpdateEnterpriseSettings } from "./UpdateEnterpriseSettings";
import { UpdateProject } from "./UpdateProject";
import { DeleteProjectForm } from "./DeleteProjectForm";
import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
import useProject from 'hooks/api/getters/useProject/useProject';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import React, { useContext } from 'react';
import AccessContext from 'contexts/AccessContext';
import { Alert, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { UpdateEnterpriseSettings } from './UpdateEnterpriseSettings';
import { UpdateProject } from './UpdateProject';
import { DeleteProjectForm } from './DeleteProjectForm';
const StyledFormContainer = styled("div")(({ theme }) => ({
display: "flex",
flexDirection: "column",
const StyledFormContainer = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
}));
const EditProject = () => {
const { isEnterprise } = useUiConfig();
const { hasAccess } = useContext(AccessContext);
const id = useRequiredPathParam("projectId");
const id = useRequiredPathParam('projectId');
const { project } = useProject(id);
const accessDeniedAlert = !hasAccess(UPDATE_PROJECT, id) && (
<Alert severity="error" sx={{ mb: 4 }}>
<Alert severity='error' sx={{ mb: 4 }}>
You do not have the required permissions to edit this project.
</Alert>
);

View File

@ -1,34 +1,34 @@
import React, { useEffect } from "react";
import useUiConfig from "hooks/api/getters/useUiConfig/useUiConfig";
import useToast from "hooks/useToast";
import { useRequiredPathParam } from "hooks/useRequiredPathParam";
import useProjectEnterpriseSettingsForm from "component/project/Project/hooks/useProjectEnterpriseSettingsForm";
import useProject from "hooks/api/getters/useProject/useProject";
import useProjectApi from "hooks/api/actions/useProjectApi/useProjectApi";
import { formatUnknownError } from "utils/formatUnknownError";
import FormTemplate from "component/common/FormTemplate/FormTemplate";
import ProjectEnterpriseSettingsForm from "component/project/Project/ProjectEnterpriseSettingsForm/ProjectEnterpriseSettingsForm";
import PermissionButton from "component/common/PermissionButton/PermissionButton";
import { UPDATE_PROJECT } from "component/providers/AccessProvider/permissions";
import { IProject } from "component/../interfaces/project";
import { styled } from "@mui/material";
import { usePlausibleTracker } from "hooks/usePlausibleTracker";
import React, { useEffect } from 'react';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import useProjectEnterpriseSettingsForm from 'component/project/Project/hooks/useProjectEnterpriseSettingsForm';
import useProject from 'hooks/api/getters/useProject/useProject';
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
import { formatUnknownError } from 'utils/formatUnknownError';
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import ProjectEnterpriseSettingsForm from 'component/project/Project/ProjectEnterpriseSettingsForm/ProjectEnterpriseSettingsForm';
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
import { IProject } from 'component/../interfaces/project';
import { styled } from '@mui/material';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
const StyledContainer = styled("div")(({ theme }) => ({
const StyledContainer = styled('div')(({ theme }) => ({
minHeight: 0,
borderRadius: theme.spacing(2),
border: `1px solid ${theme.palette.divider}`,
width: "100%",
display: "flex",
margin: "0 auto",
overflow: "hidden",
width: '100%',
display: 'flex',
margin: '0 auto',
overflow: 'hidden',
[theme.breakpoints.down(1100)]: {
flexDirection: "column",
flexDirection: 'column',
minHeight: 0,
},
}));
const StyledFormContainer = styled("div")(({ theme }) => ({
const StyledFormContainer = styled('div')(({ theme }) => ({
borderTop: `1px solid ${theme.palette.divider}`,
paddingTop: theme.spacing(4),
}));
@ -36,17 +36,17 @@ const StyledFormContainer = styled("div")(({ theme }) => ({
interface IUpdateEnterpriseSettings {
project: IProject;
}
const EDIT_PROJECT_SETTINGS_BTN = "EDIT_PROJECT_SETTINGS_BTN";
const EDIT_PROJECT_SETTINGS_BTN = 'EDIT_PROJECT_SETTINGS_BTN';
export const useModeTracking = () => {
const [previousMode, setPreviousMode] = React.useState<string>("");
const [previousMode, setPreviousMode] = React.useState<string>('');
const { trackEvent } = usePlausibleTracker();
const eventName = "project-mode" as const;
const eventName = 'project-mode' as const;
const trackModePattern = (newMode: string) => {
if (newMode !== previousMode) {
trackEvent(eventName, {
props: { mode: newMode, action: "updated" },
props: { mode: newMode, action: 'updated' },
});
}
};
@ -59,7 +59,7 @@ export const UpdateEnterpriseSettings = ({
}: IUpdateEnterpriseSettings) => {
const { uiConfig } = useUiConfig();
const { setToastData, setToastApiError } = useToast();
const id = useRequiredPathParam("projectId");
const id = useRequiredPathParam('projectId');
const {
projectMode,
@ -77,7 +77,7 @@ export const UpdateEnterpriseSettings = ({
project.mode,
project?.featureNaming?.pattern,
project?.featureNaming?.example,
project?.featureNaming?.description
project?.featureNaming?.description,
);
const formatProjectSettingsApiCode = () => {
@ -94,20 +94,20 @@ export const UpdateEnterpriseSettings = ({
const useFeatureNamePatternTracking = () => {
const [previousPattern, setPreviousPattern] =
React.useState<string>("");
React.useState<string>('');
const { trackEvent } = usePlausibleTracker();
const eventName = "feature-naming-pattern" as const;
const eventName = 'feature-naming-pattern' as const;
const trackPattern = (newPattern: string = "") => {
const trackPattern = (newPattern: string = '') => {
if (newPattern === previousPattern) {
// do nothing; they've probably updated something else in the
// project.
} else if (newPattern === "" && previousPattern !== "") {
trackEvent(eventName, { props: { action: "removed" } });
} else if (newPattern !== "" && previousPattern === "") {
trackEvent(eventName, { props: { action: "added" } });
} else if (newPattern !== "" && previousPattern !== "") {
trackEvent(eventName, { props: { action: "edited" } });
} else if (newPattern === '' && previousPattern !== '') {
trackEvent(eventName, { props: { action: 'removed' } });
} else if (newPattern !== '' && previousPattern === '') {
trackEvent(eventName, { props: { action: 'added' } });
} else if (newPattern !== '' && previousPattern !== '') {
trackEvent(eventName, { props: { action: 'edited' } });
}
};
@ -126,8 +126,8 @@ export const UpdateEnterpriseSettings = ({
await editProjectSettings(id, payload);
refetch();
setToastData({
title: "Project information updated",
type: "success",
title: 'Project information updated',
type: 'success',
});
trackPattern(featureNamingPattern);
trackModePattern(projectMode);
@ -137,7 +137,7 @@ export const UpdateEnterpriseSettings = ({
};
useEffect(() => {
setPreviousPattern(featureNamingPattern || "");
setPreviousPattern(featureNamingPattern || '');
setPreviousMode(projectMode);
}, [project]);
@ -145,10 +145,10 @@ export const UpdateEnterpriseSettings = ({
<StyledContainer>
<FormTemplate
loading={loading}
title="Enterprise Settings"
description=""
documentationLink="https://docs.getunleash.io/reference/projects"
documentationLinkLabel="Projects documentation"
title='Enterprise Settings'
description=''
documentationLink='https://docs.getunleash.io/reference/projects'
documentationLinkLabel='Projects documentation'
formatApiCode={formatProjectSettingsApiCode}
compactPadding
showDescription={false}
@ -172,7 +172,7 @@ export const UpdateEnterpriseSettings = ({
clearErrors={clearSettingsErrors}
>
<PermissionButton
type="submit"
type='submit'
permission={UPDATE_PROJECT}
projectId={id}
data-testid={EDIT_PROJECT_SETTINGS_BTN}

View File

@ -1,39 +1,39 @@
import FormTemplate from "component/common/FormTemplate/FormTemplate";
import ProjectForm from "../../../ProjectForm/ProjectForm";
import PermissionButton from "component/common/PermissionButton/PermissionButton";
import { UPDATE_PROJECT } from "component/providers/AccessProvider/permissions";
import React from "react";
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import ProjectForm from '../../../ProjectForm/ProjectForm';
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
import React from 'react';
import useProjectForm, {
DEFAULT_PROJECT_STICKINESS,
} from "../../../hooks/useProjectForm";
import { useDefaultProjectSettings } from "hooks/useDefaultProjectSettings";
import { formatUnknownError } from "utils/formatUnknownError";
import useToast from "hooks/useToast";
import { usePlausibleTracker } from "hooks/usePlausibleTracker";
import useProjectApi from "hooks/api/actions/useProjectApi/useProjectApi";
import useUiConfig from "hooks/api/getters/useUiConfig/useUiConfig";
import { IProject } from "interfaces/project";
import useProject from "hooks/api/getters/useProject/useProject";
import { useRequiredPathParam } from "hooks/useRequiredPathParam";
import { styled } from "@mui/material";
} from '../../../hooks/useProjectForm';
import { useDefaultProjectSettings } from 'hooks/useDefaultProjectSettings';
import { formatUnknownError } from 'utils/formatUnknownError';
import useToast from 'hooks/useToast';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { IProject } from 'interfaces/project';
import useProject from 'hooks/api/getters/useProject/useProject';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { styled } from '@mui/material';
const StyledContainer = styled("div")<{ isPro: boolean }>(
const StyledContainer = styled('div')<{ isPro: boolean }>(
({ theme, isPro }) => ({
minHeight: 0,
borderRadius: theme.spacing(2),
border: isPro ? "0" : `1px solid ${theme.palette.divider}`,
width: "100%",
display: "flex",
margin: "0 auto",
overflow: "hidden",
border: isPro ? '0' : `1px solid ${theme.palette.divider}`,
width: '100%',
display: 'flex',
margin: '0 auto',
overflow: 'hidden',
[theme.breakpoints.down(1100)]: {
flexDirection: "column",
flexDirection: 'column',
minHeight: 0,
},
})
}),
);
const StyledFormContainer = styled("div")(({ theme }) => ({
const StyledFormContainer = styled('div')(({ theme }) => ({
borderTop: `1px solid ${theme.palette.divider}`,
paddingTop: theme.spacing(4),
}));
@ -41,9 +41,9 @@ const StyledFormContainer = styled("div")(({ theme }) => ({
interface IUpdateProject {
project: IProject;
}
const EDIT_PROJECT_BTN = "EDIT_PROJECT_BTN";
const EDIT_PROJECT_BTN = 'EDIT_PROJECT_BTN';
export const UpdateProject = ({ project }: IUpdateProject) => {
const id = useRequiredPathParam("projectId");
const id = useRequiredPathParam('projectId');
const { uiConfig, isPro } = useUiConfig();
const { setToastData, setToastApiError } = useToast();
const { defaultStickiness } = useDefaultProjectSettings(id);
@ -69,7 +69,7 @@ export const UpdateProject = ({ project }: IUpdateProject) => {
project.name,
project.description,
defaultStickiness,
String(project.featureLimit)
String(project.featureLimit),
);
const { editProject, loading } = useProjectApi();
@ -94,11 +94,11 @@ export const UpdateProject = ({ project }: IUpdateProject) => {
await editProject(id, payload);
refetch();
setToastData({
title: "Project information updated",
type: "success",
title: 'Project information updated',
type: 'success',
});
if (projectStickiness !== DEFAULT_PROJECT_STICKINESS) {
trackEvent("project_stickiness_set");
trackEvent('project_stickiness_set');
}
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
@ -110,10 +110,10 @@ export const UpdateProject = ({ project }: IUpdateProject) => {
<StyledContainer isPro={isPro()}>
<FormTemplate
loading={loading}
title="General Settings"
description="Projects allows you to group feature toggles together in the management UI."
documentationLink="https://docs.getunleash.io/reference/projects"
documentationLinkLabel="Projects documentation"
title='General Settings'
description='Projects allows you to group feature toggles together in the management UI.'
documentationLink='https://docs.getunleash.io/reference/projects'
documentationLinkLabel='Projects documentation'
formatApiCode={formatProjectApiCode}
compactPadding
compact
@ -132,12 +132,12 @@ export const UpdateProject = ({ project }: IUpdateProject) => {
featureLimit={featureLimit}
projectDesc={projectDesc}
setProjectDesc={setProjectDesc}
mode="Edit"
mode='Edit'
clearErrors={clearErrors}
validateProjectId={validateProjectId}
>
<PermissionButton
type="submit"
type='submit'
permission={UPDATE_PROJECT}
projectId={projectId}
data-testid={EDIT_PROJECT_BTN}