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:
parent
c3540e1396
commit
f9bca20c78
@ -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>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
</>
|
||||
<>
|
||||
|
@ -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 = '',
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -65,6 +65,7 @@ export type UiFlags = {
|
||||
featureNamingPattern?: boolean;
|
||||
doraMetrics?: boolean;
|
||||
variantTypeNumber?: boolean;
|
||||
privateProjects?: boolean;
|
||||
[key: string]: boolean | Variant | undefined;
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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.",
|
||||
|
@ -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.",
|
||||
|
@ -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.",
|
||||
|
@ -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({
|
||||
|
@ -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 = {
|
||||
|
@ -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;
|
||||
|
@ -43,6 +43,7 @@ process.nextTick(async () => {
|
||||
featureNamingPattern: true,
|
||||
doraMetrics: true,
|
||||
variantTypeNumber: true,
|
||||
privateProjects: true,
|
||||
},
|
||||
},
|
||||
authentication: {
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user