1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

Feat/project private mode (#4743)

Adds `private` option to project mode
Update schemas and models to accept and persist

Closes #
[1-1366](https://linear.app/unleash/issue/1-1366/introduce-private-in-collaboration-mode)

---------

Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
andreas-unleash 2023-09-15 11:01:25 +03:00 committed by GitHub
parent c3540e1396
commit f9bca20c78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 99 additions and 37 deletions

View File

@ -1,6 +1,8 @@
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,
@ -11,25 +13,40 @@ const StyledDescription = styled(Typography)(({ theme }) => ({
color: theme.palette.text.secondary,
}));
export const CollaborationModeTooltip: FC = () => (
<HelpIcon
htmlTooltip
tooltip={
<>
<Box>
<StyledTitle>open: </StyledTitle>
<StyledDescription>
everyone can submit change requests
</StyledDescription>
</Box>
<Box sx={{ mt: 2 }}>
<StyledTitle>protected: </StyledTitle>
<StyledDescription>
only admins and project members can submit change
requests
</StyledDescription>
</Box>
</>
}
/>
);
export const CollaborationModeTooltip: FC = () => {
const privateProjects = useUiFlag('privateProjects');
return (
<HelpIcon
htmlTooltip
tooltip={
<>
<Box>
<StyledTitle>open: </StyledTitle>
<StyledDescription>
everyone can submit change requests
</StyledDescription>
</Box>
<Box sx={{ mt: 2 }}>
<StyledTitle>protected: </StyledTitle>
<StyledDescription>
only admins and project members can submit change
requests
</StyledDescription>
</Box>
<ConditionallyRender
condition={Boolean(privateProjects)}
show={
<Box sx={{ mt: 2 }}>
<StyledTitle>private: </StyledTitle>
<StyledDescription>
only projects members can and access see the
project
</StyledDescription>
</Box>
}
/>
</>
}
/>
);
};

View File

@ -11,6 +11,7 @@ import { FeatureTogglesLimitTooltip } from './FeatureTogglesLimitTooltip';
import { FeatureFlagNamingTooltip } from './FeatureFlagNamingTooltip';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { useUiFlag } from 'hooks/useUiFlag';
interface IProjectForm {
projectId: string;
@ -182,6 +183,19 @@ const ProjectForm: React.FC<IProjectForm> = ({
const { setPreviousPattern, trackPattern } =
useFeatureNamePatternTracking();
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' },
];
useEffect(() => {
setPreviousPattern(featureNamingPattern || '');
}, [projectId]);
@ -342,10 +356,7 @@ const ProjectForm: React.FC<IProjectForm> = ({
onChange={e => {
setProjectMode?.(e.target.value as ProjectMode);
}}
options={[
{ key: 'open', label: 'open' },
{ key: 'protected', label: 'protected' },
]}
options={projectModeOptions}
></StyledSelect>
</>
<>

View File

@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
import { formatUnknownError } from 'utils/formatUnknownError';
export type ProjectMode = 'open' | 'protected';
export type ProjectMode = 'open' | 'protected' | 'private';
export const DEFAULT_PROJECT_STICKINESS = 'default';
const useProjectForm = (
initialProjectId = '',

View File

@ -1,11 +1,12 @@
import type { BatchStaleSchema, CreateFeatureStrategySchema } from 'openapi';
import useAPI from '../useApi/useApi';
import { ProjectMode } from 'component/project/Project/hooks/useProjectForm';
interface ICreatePayload {
id: string;
name: string;
description: string;
mode: 'open' | 'protected';
mode: ProjectMode;
defaultStickiness: string;
}

View File

@ -1,6 +1,7 @@
import { ProjectStatsSchema } from 'openapi';
import { IFeatureToggleListItem } from './featureToggle';
import { ProjectEnvironmentType } from '../component/project/Project/ProjectFeatureToggles/hooks/useEnvironmentsRef';
import { ProjectEnvironmentType } from 'component/project/Project/ProjectFeatureToggles/hooks/useEnvironmentsRef';
import { ProjectMode } from 'component/project/Project/hooks/useProjectForm';
export interface IProjectCard {
name: string;
@ -30,7 +31,7 @@ export interface IProject {
stats: ProjectStatsSchema;
favorite: boolean;
features: IFeatureToggleListItem[];
mode: 'open' | 'protected';
mode: ProjectMode;
defaultStickiness: string;
featureLimit?: number;
featureNaming?: FeatureNamingType;

View File

@ -65,6 +65,7 @@ export type UiFlags = {
featureNamingPattern?: boolean;
doraMetrics?: boolean;
variantTypeNumber?: boolean;
privateProjects?: boolean;
[key: string]: boolean | Variant | undefined;
};

View File

@ -98,6 +98,7 @@ exports[`should create default config 1`] = `
"migrationLock": true,
"multipleRoles": false,
"personalAccessTokensKillSwitch": false,
"privateProjects": false,
"proPlanAutoCharge": false,
"responseTimeWithAppNameKillSwitch": false,
"slackAppAddon": false,
@ -134,6 +135,7 @@ exports[`should create default config 1`] = `
"migrationLock": true,
"multipleRoles": false,
"personalAccessTokensKillSwitch": false,
"privateProjects": false,
"proPlanAutoCharge": false,
"responseTimeWithAppNameKillSwitch": false,
"slackAppAddon": false,

View File

@ -54,7 +54,7 @@ export const healthOverviewSchema = {
},
mode: {
type: 'string',
enum: ['open', 'protected'],
enum: ['open', 'protected', 'private'],
example: 'open',
description:
"The project's [collaboration mode](https://docs.getunleash.io/reference/project-collaboration-mode). Determines whether non-project members can submit change requests or not.",

View File

@ -51,7 +51,7 @@ export const projectOverviewSchema = {
},
mode: {
type: 'string',
enum: ['open', 'protected'],
enum: ['open', 'protected', 'private'],
example: 'open',
description:
"The project's [collaboration mode](https://docs.getunleash.io/reference/project-collaboration-mode). Determines whether non-project members can submit change requests or not.",

View File

@ -61,7 +61,7 @@ export const projectSchema = {
},
mode: {
type: 'string',
enum: ['open', 'protected'],
enum: ['open', 'protected', 'private'],
example: 'open',
description:
"The project's [collaboration mode](https://docs.getunleash.io/reference/project-collaboration-mode). Determines whether non-project members can submit change requests or not.",

View File

@ -7,7 +7,10 @@ export const projectSchema = joi
id: nameType,
name: joi.string().required(),
description: joi.string().allow(null).allow('').optional(),
mode: joi.string().valid('open', 'protected').default('open'),
mode: joi
.string()
.valid('open', 'protected', 'private')
.default('open'),
defaultStickiness: joi.string().default('default'),
featureLimit: joi.number().allow(null).optional(),
featureNaming: joi.object().keys({

View File

@ -28,7 +28,8 @@ export type IFlagKey =
| 'multipleRoles'
| 'featureNamingPattern'
| 'doraMetrics'
| 'variantTypeNumber';
| 'variantTypeNumber'
| 'privateProjects';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
@ -132,6 +133,10 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_VARIANT_TYPE_NUMBER,
false,
),
privateProjects: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_PRIVATE_PROJECTS,
false,
),
};
export const defaultExperimentalOptions: IExperimentalOptions = {

View File

@ -187,7 +187,7 @@ export interface IFeatureOverview {
environments: IEnvironmentOverview[];
}
export type ProjectMode = 'open' | 'protected';
export type ProjectMode = 'open' | 'protected' | 'private';
export interface IFeatureNaming {
pattern: string | null;

View File

@ -43,6 +43,7 @@ process.nextTick(async () => {
featureNamingPattern: true,
doraMetrics: true,
variantTypeNumber: true,
privateProjects: true,
},
},
authentication: {

View File

@ -47,7 +47,9 @@ beforeAll(async () => {
const config = createTestConfig({
getLogger,
// @ts-ignore
experimental: { environments: { enabled: true } },
experimental: {
flags: { privateProjects: true },
},
});
groupService = new GroupService(stores, config);
accessService = new AccessService(stores, config, groupService);
@ -134,6 +136,24 @@ test('should create new project', async () => {
expect(ret.mode).toEqual('protected');
});
test('should create new private project', async () => {
const project = {
id: 'testPrivate',
name: 'New private project',
description: 'Blah',
mode: 'private' as const,
defaultStickiness: 'default',
};
await projectService.createProject(project, user);
const ret = await projectService.getProject('testPrivate');
expect(project.id).toEqual(ret.id);
expect(project.name).toEqual(ret.name);
expect(project.description).toEqual(ret.description);
expect(ret.createdAt).toBeTruthy();
expect(ret.mode).toEqual('private');
});
test('should delete project', async () => {
const project = {
id: 'test-delete',