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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,13 @@
import React, { useEffect } from "react"; import React, { useEffect } from 'react';
import { ConditionallyRender } from "component/common/ConditionallyRender/ConditionallyRender"; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import Select from "component/common/select"; import Select from 'component/common/select';
import { ProjectMode } from "../hooks/useProjectEnterpriseSettingsForm"; import { ProjectMode } from '../hooks/useProjectEnterpriseSettingsForm';
import { Box, InputAdornment, styled, TextField } from "@mui/material"; import { Box, InputAdornment, styled, TextField } from '@mui/material';
import { CollaborationModeTooltip } from "./CollaborationModeTooltip"; import { CollaborationModeTooltip } from './CollaborationModeTooltip';
import Input from "component/common/Input/Input"; import Input from 'component/common/Input/Input';
import { FeatureFlagNamingTooltip } from "./FeatureFlagNamingTooltip"; import { FeatureFlagNamingTooltip } from './FeatureFlagNamingTooltip';
import { usePlausibleTracker } from "hooks/usePlausibleTracker"; import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { useUiFlag } from "hooks/useUiFlag"; import { useUiFlag } from 'hooks/useUiFlag';
interface IProjectEnterpriseSettingsForm { interface IProjectEnterpriseSettingsForm {
projectId: string; projectId: string;
@ -24,12 +24,12 @@ interface IProjectEnterpriseSettingsForm {
clearErrors: () => void; clearErrors: () => void;
} }
const StyledForm = styled("form")(({ theme }) => ({ const StyledForm = styled('form')(({ theme }) => ({
height: "100%", height: '100%',
paddingBottom: theme.spacing(4), paddingBottom: theme.spacing(4),
})); }));
const StyledSubtitle = styled("div")(({ theme }) => ({ const StyledSubtitle = styled('div')(({ theme }) => ({
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
fontSize: theme.fontSizes.smallerBody, fontSize: theme.fontSizes.smallerBody,
lineHeight: 1.25, lineHeight: 1.25,
@ -37,42 +37,42 @@ const StyledSubtitle = styled("div")(({ theme }) => ({
})); }));
const StyledInput = styled(Input)(({ theme }) => ({ const StyledInput = styled(Input)(({ theme }) => ({
width: "100%", width: '100%',
marginBottom: theme.spacing(2), marginBottom: theme.spacing(2),
paddingRight: theme.spacing(1), paddingRight: theme.spacing(1),
})); }));
const StyledTextField = styled(TextField)(({ theme }) => ({ const StyledTextField = styled(TextField)(({ theme }) => ({
width: "100%", width: '100%',
marginBottom: theme.spacing(2), marginBottom: theme.spacing(2),
})); }));
const StyledFieldset = styled("fieldset")(() => ({ const StyledFieldset = styled('fieldset')(() => ({
padding: 0, padding: 0,
border: "none", border: 'none',
})); }));
const StyledSelect = styled(Select)(({ theme }) => ({ const StyledSelect = styled(Select)(({ theme }) => ({
marginBottom: theme.spacing(2), marginBottom: theme.spacing(2),
minWidth: "200px", minWidth: '200px',
})); }));
const StyledButtonContainer = styled("div")(() => ({ const StyledButtonContainer = styled('div')(() => ({
marginTop: "auto", marginTop: 'auto',
display: "flex", display: 'flex',
justifyContent: "flex-end", justifyContent: 'flex-end',
})); }));
const StyledFlagNamingContainer = styled("div")(({ theme }) => ({ const StyledFlagNamingContainer = styled('div')(({ theme }) => ({
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
alignItems: "flex-start", alignItems: 'flex-start',
gap: theme.spacing(1), gap: theme.spacing(1),
"& > *": { width: "100%" }, '& > *': { width: '100%' },
})); }));
const StyledPatternNamingExplanation = styled("div")(({ theme }) => ({ const StyledPatternNamingExplanation = styled('div')(({ theme }) => ({
"p + p": { marginTop: theme.spacing(1) }, 'p + p': { marginTop: theme.spacing(1) },
})); }));
export const validateFeatureNamingExample = ({ export const validateFeatureNamingExample = ({
@ -83,285 +83,288 @@ export const validateFeatureNamingExample = ({
pattern: string; pattern: string;
example: string; example: string;
featureNamingPatternError: string | undefined; featureNamingPatternError: string | undefined;
}): { state: "valid" } | { state: "invalid"; reason: string } => { }): { state: 'valid' } | { state: 'invalid'; reason: string } => {
if (featureNamingPatternError || !example || !pattern) { if (featureNamingPatternError || !example || !pattern) {
return { state: "valid" }; return { state: 'valid' };
} else if (example && pattern) { } else if (example && pattern) {
const regex = new RegExp(`^${pattern}$`); const regex = new RegExp(`^${pattern}$`);
const matches = regex.test(example); const matches = regex.test(example);
if (!matches) { if (!matches) {
return { state: "invalid", reason: "Example does not match regex" }; return { state: 'invalid', reason: 'Example does not match regex' };
} else { } else {
return { state: "valid" }; return { state: 'valid' };
} }
} }
return { state: "valid" }; return { state: 'valid' };
}; };
const useFeatureNamePatternTracking = () => { const useFeatureNamePatternTracking = () => {
const [previousPattern, setPreviousPattern] = React.useState<string>(""); const [previousPattern, setPreviousPattern] = React.useState<string>('');
const { trackEvent } = usePlausibleTracker(); 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) { if (pattern === previousPattern) {
// do nothing; they've probably updated something else in the // do nothing; they've probably updated something else in the
// project. // project.
} else if (pattern === "" && previousPattern !== "") { } else if (pattern === '' && previousPattern !== '') {
trackEvent(eventName, { props: { action: "removed" } }); trackEvent(eventName, { props: { action: 'removed' } });
} else if (pattern !== "" && previousPattern === "") { } else if (pattern !== '' && previousPattern === '') {
trackEvent(eventName, { props: { action: "added" } }); trackEvent(eventName, { props: { action: 'added' } });
} else if (pattern !== "" && previousPattern !== "") { } else if (pattern !== '' && previousPattern !== '') {
trackEvent(eventName, { props: { action: "edited" } }); trackEvent(eventName, { props: { action: 'edited' } });
} }
}; };
return { trackPattern, setPreviousPattern }; return { trackPattern, setPreviousPattern };
}; };
const ProjectEnterpriseSettingsForm: React.FC< const ProjectEnterpriseSettingsForm: React.FC<IProjectEnterpriseSettingsForm> =
IProjectEnterpriseSettingsForm ({
> = ({ children,
children, handleSubmit,
handleSubmit, projectId,
projectId, projectMode,
projectMode, featureNamingExample,
featureNamingExample, featureNamingPattern,
featureNamingPattern, featureNamingDescription,
featureNamingDescription, setFeatureNamingExample,
setFeatureNamingExample, setFeatureNamingPattern,
setFeatureNamingPattern, setFeatureNamingDescription,
setFeatureNamingDescription, setProjectMode,
setProjectMode, errors,
errors, clearErrors,
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 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, pattern,
example, }: {
featureNamingPatternError: errors.featureNamingPattern, example: string;
}); pattern: string;
}) => {
const validationResult = validateFeatureNamingExample({
pattern,
example,
featureNamingPatternError: errors.featureNamingPattern,
});
switch (validationResult.state) { switch (validationResult.state) {
case "invalid": case 'invalid':
errors.namingExample = validationResult.reason; errors.namingExample = validationResult.reason;
break; break;
case "valid": case 'valid':
delete errors.namingExample; delete errors.namingExample;
break; 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";
} }
} };
setFeatureNamingPattern?.(regex);
updateNamingExampleError({
pattern: regex,
example: featureNamingExample || "",
});
};
const onSetFeatureNamingExample = (example: string) => { const onSetFeatureNamingPattern = (regex: string) => {
setFeatureNamingExample && setFeatureNamingExample(example); const disallowedStrings = [
updateNamingExampleError({ ' ',
pattern: featureNamingPattern || "", '\\t',
example, '\\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) => { const onSetFeatureNamingExample = (example: string) => {
setFeatureNamingDescription?.(description); setFeatureNamingExample && setFeatureNamingExample(example);
}; updateNamingExampleError({
pattern: featureNamingPattern || '',
example,
});
};
return ( const onSetFeatureNamingDescription = (description: string) => {
<StyledForm setFeatureNamingDescription?.(description);
onSubmit={(submitEvent) => { };
handleSubmit(submitEvent);
trackPattern(featureNamingPattern); return (
}} <StyledForm
> onSubmit={(submitEvent) => {
<> handleSubmit(submitEvent);
<Box trackPattern(featureNamingPattern);
sx={{ }}
display: "flex", >
alignItems: "center", <>
marginBottom: 1, <Box
gap: 1, sx={{
}} display: 'flex',
> alignItems: 'center',
<p>What is your project collaboration mode?</p> marginBottom: 1,
<CollaborationModeTooltip /> gap: 1,
</Box> }}
<StyledSelect >
id="project-mode" <p>What is your project collaboration mode?</p>
value={projectMode} <CollaborationModeTooltip />
label="Project collaboration mode" </Box>
name="Project collaboration mode" <StyledSelect
onChange={(e) => { id='project-mode'
setProjectMode?.(e.target.value as ProjectMode); value={projectMode}
}} label='Project collaboration mode'
options={projectModeOptions} name='Project collaboration mode'
/> onChange={(e) => {
</> setProjectMode?.(e.target.value as ProjectMode);
<ConditionallyRender }}
condition={Boolean(shouldShowFlagNaming)} options={projectModeOptions}
show={ />
<StyledFieldset> </>
<Box <ConditionallyRender
sx={{ condition={Boolean(shouldShowFlagNaming)}
display: "flex", show={
alignItems: "center", <StyledFieldset>
marginBottom: 1, <Box
gap: 1, sx={{
}} display: 'flex',
> alignItems: 'center',
<legend>Feature flag naming pattern?</legend> marginBottom: 1,
<FeatureFlagNamingTooltip /> gap: 1,
</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>
),
}} }}
type={"text"} >
value={featureNamingPattern || ""} <legend>Feature flag naming pattern?</legend>
error={Boolean(errors.featureNamingPattern)} <FeatureFlagNamingTooltip />
errorText={errors.featureNamingPattern} </Box>
onChange={(e) =>
onSetFeatureNamingPattern(e.target.value)
}
/>
<StyledSubtitle> <StyledSubtitle>
<p id="pattern-additional-description"> <StyledPatternNamingExplanation id='pattern-naming-description'>
The example and description will be shown to <p>
users when they create a new feature flag in Define a{' '}
this project. <a
</p> 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> </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 <StyledInput
label={"Naming Example"} label={'Naming Example'}
name="feature flag naming example" name='feature flag naming example'
type={"text"} type={'text'}
aria-describedby="pattern-additional-description" aria-describedby='pattern-additional-description'
value={featureNamingExample || ""} value={featureNamingExample || ''}
placeholder="dx.feature1.1-135" placeholder='dx.feature1.1-135'
error={Boolean(errors.namingExample)} error={Boolean(errors.namingExample)}
errorText={errors.namingExample} errorText={errors.namingExample}
onChange={(e) => onChange={(e) =>
onSetFeatureNamingExample(e.target.value) onSetFeatureNamingExample(
} e.target.value,
/> )
<StyledTextField }
label={"Naming pattern description"} />
name="feature flag naming description" <StyledTextField
type={"text"} label={'Naming pattern description'}
aria-describedby="pattern-additional-description" name='feature flag naming description'
placeholder={`<project>.<featureName>.<ticket> 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.`} The flag name should contain the project name, the feature name, and the ticket number, each separated by a dot.`}
multiline multiline
minRows={5} minRows={5}
value={featureNamingDescription || ""} value={featureNamingDescription || ''}
onChange={(e) => onChange={(e) =>
onSetFeatureNamingDescription( onSetFeatureNamingDescription(
e.target.value e.target.value,
) )
} }
/> />
</StyledFlagNamingContainer> </StyledFlagNamingContainer>
</StyledFieldset> </StyledFieldset>
} }
/> />
<StyledButtonContainer>{children}</StyledButtonContainer> <StyledButtonContainer>{children}</StyledButtonContainer>
</StyledForm> </StyledForm>
); );
}; };
export default ProjectEnterpriseSettingsForm; export default ProjectEnterpriseSettingsForm;

View File

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

View File

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

View File

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

View File

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

View File

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