mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-18 01:18:23 +02: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 { 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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
const StyledTitle = styled(Typography)(({ theme }) => ({
|
const StyledTitle = styled(Typography)(({ theme }) => ({
|
||||||
fontWeight: theme.fontWeight.bold,
|
fontWeight: theme.fontWeight.bold,
|
||||||
@ -11,7 +13,9 @@ const StyledDescription = styled(Typography)(({ theme }) => ({
|
|||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const CollaborationModeTooltip: FC = () => (
|
export const CollaborationModeTooltip: FC = () => {
|
||||||
|
const privateProjects = useUiFlag('privateProjects');
|
||||||
|
return (
|
||||||
<HelpIcon
|
<HelpIcon
|
||||||
htmlTooltip
|
htmlTooltip
|
||||||
tooltip={
|
tooltip={
|
||||||
@ -29,7 +33,20 @@ export const CollaborationModeTooltip: FC = () => (
|
|||||||
requests
|
requests
|
||||||
</StyledDescription>
|
</StyledDescription>
|
||||||
</Box>
|
</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 { FeatureFlagNamingTooltip } from './FeatureFlagNamingTooltip';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
interface IProjectForm {
|
interface IProjectForm {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -182,6 +183,19 @@ const ProjectForm: React.FC<IProjectForm> = ({
|
|||||||
const { setPreviousPattern, trackPattern } =
|
const { setPreviousPattern, trackPattern } =
|
||||||
useFeatureNamePatternTracking();
|
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(() => {
|
useEffect(() => {
|
||||||
setPreviousPattern(featureNamingPattern || '');
|
setPreviousPattern(featureNamingPattern || '');
|
||||||
}, [projectId]);
|
}, [projectId]);
|
||||||
@ -342,10 +356,7 @@ const ProjectForm: React.FC<IProjectForm> = ({
|
|||||||
onChange={e => {
|
onChange={e => {
|
||||||
setProjectMode?.(e.target.value as ProjectMode);
|
setProjectMode?.(e.target.value as ProjectMode);
|
||||||
}}
|
}}
|
||||||
options={[
|
options={projectModeOptions}
|
||||||
{ key: 'open', label: 'open' },
|
|
||||||
{ key: 'protected', label: 'protected' },
|
|
||||||
]}
|
|
||||||
></StyledSelect>
|
></StyledSelect>
|
||||||
</>
|
</>
|
||||||
<>
|
<>
|
||||||
|
@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
|
|||||||
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';
|
||||||
|
|
||||||
export type ProjectMode = 'open' | 'protected';
|
export type ProjectMode = 'open' | 'protected' | 'private';
|
||||||
export const DEFAULT_PROJECT_STICKINESS = 'default';
|
export const DEFAULT_PROJECT_STICKINESS = 'default';
|
||||||
const useProjectForm = (
|
const useProjectForm = (
|
||||||
initialProjectId = '',
|
initialProjectId = '',
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import type { BatchStaleSchema, CreateFeatureStrategySchema } from 'openapi';
|
import type { BatchStaleSchema, CreateFeatureStrategySchema } from 'openapi';
|
||||||
import useAPI from '../useApi/useApi';
|
import useAPI from '../useApi/useApi';
|
||||||
|
import { ProjectMode } from 'component/project/Project/hooks/useProjectForm';
|
||||||
|
|
||||||
interface ICreatePayload {
|
interface ICreatePayload {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
mode: 'open' | 'protected';
|
mode: ProjectMode;
|
||||||
defaultStickiness: string;
|
defaultStickiness: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ProjectStatsSchema } from 'openapi';
|
import { ProjectStatsSchema } from 'openapi';
|
||||||
import { IFeatureToggleListItem } from './featureToggle';
|
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 {
|
export interface IProjectCard {
|
||||||
name: string;
|
name: string;
|
||||||
@ -30,7 +31,7 @@ export interface IProject {
|
|||||||
stats: ProjectStatsSchema;
|
stats: ProjectStatsSchema;
|
||||||
favorite: boolean;
|
favorite: boolean;
|
||||||
features: IFeatureToggleListItem[];
|
features: IFeatureToggleListItem[];
|
||||||
mode: 'open' | 'protected';
|
mode: ProjectMode;
|
||||||
defaultStickiness: string;
|
defaultStickiness: string;
|
||||||
featureLimit?: number;
|
featureLimit?: number;
|
||||||
featureNaming?: FeatureNamingType;
|
featureNaming?: FeatureNamingType;
|
||||||
|
@ -65,6 +65,7 @@ export type UiFlags = {
|
|||||||
featureNamingPattern?: boolean;
|
featureNamingPattern?: boolean;
|
||||||
doraMetrics?: boolean;
|
doraMetrics?: boolean;
|
||||||
variantTypeNumber?: boolean;
|
variantTypeNumber?: boolean;
|
||||||
|
privateProjects?: boolean;
|
||||||
[key: string]: boolean | Variant | undefined;
|
[key: string]: boolean | Variant | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -98,6 +98,7 @@ exports[`should create default config 1`] = `
|
|||||||
"migrationLock": true,
|
"migrationLock": true,
|
||||||
"multipleRoles": false,
|
"multipleRoles": false,
|
||||||
"personalAccessTokensKillSwitch": false,
|
"personalAccessTokensKillSwitch": false,
|
||||||
|
"privateProjects": false,
|
||||||
"proPlanAutoCharge": false,
|
"proPlanAutoCharge": false,
|
||||||
"responseTimeWithAppNameKillSwitch": false,
|
"responseTimeWithAppNameKillSwitch": false,
|
||||||
"slackAppAddon": false,
|
"slackAppAddon": false,
|
||||||
@ -134,6 +135,7 @@ exports[`should create default config 1`] = `
|
|||||||
"migrationLock": true,
|
"migrationLock": true,
|
||||||
"multipleRoles": false,
|
"multipleRoles": false,
|
||||||
"personalAccessTokensKillSwitch": false,
|
"personalAccessTokensKillSwitch": false,
|
||||||
|
"privateProjects": false,
|
||||||
"proPlanAutoCharge": false,
|
"proPlanAutoCharge": false,
|
||||||
"responseTimeWithAppNameKillSwitch": false,
|
"responseTimeWithAppNameKillSwitch": false,
|
||||||
"slackAppAddon": false,
|
"slackAppAddon": false,
|
||||||
|
@ -54,7 +54,7 @@ export const healthOverviewSchema = {
|
|||||||
},
|
},
|
||||||
mode: {
|
mode: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
enum: ['open', 'protected'],
|
enum: ['open', 'protected', 'private'],
|
||||||
example: 'open',
|
example: 'open',
|
||||||
description:
|
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.",
|
"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: {
|
mode: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
enum: ['open', 'protected'],
|
enum: ['open', 'protected', 'private'],
|
||||||
example: 'open',
|
example: 'open',
|
||||||
description:
|
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.",
|
"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: {
|
mode: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
enum: ['open', 'protected'],
|
enum: ['open', 'protected', 'private'],
|
||||||
example: 'open',
|
example: 'open',
|
||||||
description:
|
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.",
|
"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,
|
id: nameType,
|
||||||
name: joi.string().required(),
|
name: joi.string().required(),
|
||||||
description: joi.string().allow(null).allow('').optional(),
|
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'),
|
defaultStickiness: joi.string().default('default'),
|
||||||
featureLimit: joi.number().allow(null).optional(),
|
featureLimit: joi.number().allow(null).optional(),
|
||||||
featureNaming: joi.object().keys({
|
featureNaming: joi.object().keys({
|
||||||
|
@ -28,7 +28,8 @@ export type IFlagKey =
|
|||||||
| 'multipleRoles'
|
| 'multipleRoles'
|
||||||
| 'featureNamingPattern'
|
| 'featureNamingPattern'
|
||||||
| 'doraMetrics'
|
| 'doraMetrics'
|
||||||
| 'variantTypeNumber';
|
| 'variantTypeNumber'
|
||||||
|
| 'privateProjects';
|
||||||
|
|
||||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||||
|
|
||||||
@ -132,6 +133,10 @@ const flags: IFlags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_VARIANT_TYPE_NUMBER,
|
process.env.UNLEASH_EXPERIMENTAL_VARIANT_TYPE_NUMBER,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
privateProjects: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_PRIVATE_PROJECTS,
|
||||||
|
false,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||||
|
@ -187,7 +187,7 @@ export interface IFeatureOverview {
|
|||||||
environments: IEnvironmentOverview[];
|
environments: IEnvironmentOverview[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProjectMode = 'open' | 'protected';
|
export type ProjectMode = 'open' | 'protected' | 'private';
|
||||||
|
|
||||||
export interface IFeatureNaming {
|
export interface IFeatureNaming {
|
||||||
pattern: string | null;
|
pattern: string | null;
|
||||||
|
@ -43,6 +43,7 @@ process.nextTick(async () => {
|
|||||||
featureNamingPattern: true,
|
featureNamingPattern: true,
|
||||||
doraMetrics: true,
|
doraMetrics: true,
|
||||||
variantTypeNumber: true,
|
variantTypeNumber: true,
|
||||||
|
privateProjects: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
|
@ -47,7 +47,9 @@ beforeAll(async () => {
|
|||||||
const config = createTestConfig({
|
const config = createTestConfig({
|
||||||
getLogger,
|
getLogger,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
experimental: { environments: { enabled: true } },
|
experimental: {
|
||||||
|
flags: { privateProjects: true },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
groupService = new GroupService(stores, config);
|
groupService = new GroupService(stores, config);
|
||||||
accessService = new AccessService(stores, config, groupService);
|
accessService = new AccessService(stores, config, groupService);
|
||||||
@ -134,6 +136,24 @@ test('should create new project', async () => {
|
|||||||
expect(ret.mode).toEqual('protected');
|
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 () => {
|
test('should delete project', async () => {
|
||||||
const project = {
|
const project = {
|
||||||
id: 'test-delete',
|
id: 'test-delete',
|
||||||
|
Loading…
Reference in New Issue
Block a user