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

chore: Backport 4.22.3 (#3508)

<!-- Thanks for creating a PR! To make it easier for reviewers and
everyone else to understand what your changes relate to, please add some
relevant content to the headings below. Feel free to ignore or delete
sections that you don't think are relevant. Thank you! ❤️ -->
Backports stickiness fixes 
## About the changes
<!-- Describe the changes introduced. What are they and why are they
being introduced? Feel free to also add screenshots or steps to view the
changes if they're visual. -->

<!-- Does it close an issue? Multiple? -->
Closes #

<!-- (For internal contributors): Does it relate to an issue on public
roadmap? -->
<!--
Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item:
#
-->

### Important files
<!-- PRs can contain a lot of changes, but not all changes are equally
important. Where should a reviewer start looking to get an overview of
the changes? Are any files particularly important? -->


## Discussion points
<!-- Anything about the PR you'd like to discuss before it gets merged?
Got any questions or doubts? -->

---------

Signed-off-by: andreas-unleash <andreas@getunleash.ai>
Co-authored-by: Gastón Fournier <gaston@getunleash.io>
Co-authored-by: GitHub Actions Bot <>
Co-authored-by: Mateusz Kwasniewski <kwasniewski.mateusz@gmail.com>
This commit is contained in:
andreas-unleash 2023-04-12 16:22:13 +03:00 committed by GitHub
parent 014a8a2280
commit 60a2c1a996
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 81 additions and 62 deletions

View File

@ -15,7 +15,6 @@ describe('notifications', () => {
cy.runBefore();
});
// This one is failing on CI: https://github.com/Unleash/unleash/actions/runs/4609305167/jobs/8160244872#step:4:193
it.skip('should create a notification when a feature is created in a project', () => {
cy.login_UI();
cy.createUser_API(userName, EDITOR).then(value => {
@ -41,7 +40,7 @@ describe('notifications', () => {
cy.get("[data-testid='NOTIFICATIONS_BUTTON']").click();
//then
// cy.get("[data-testid='UNREAD_NOTIFICATIONS']").should('exist');
cy.get("[data-testid='UNREAD_NOTIFICATIONS']").should('exist');
cy.get("[data-testid='NOTIFICATIONS_LIST']").should(
'contain.text',
`New feature ${featureToggleName}`

View File

@ -74,7 +74,7 @@ export const FeatureStrategyCreate = () => {
forceRefreshCache(feature);
ref.current = feature;
}
}, [feature]);
}, [feature.name]);
useEffect(() => {
if (strategyDefinition) {

View File

@ -273,7 +273,13 @@ export const EnvironmentVariantsModal = ({
isChangeRequestConfigured(environment?.name || '') &&
uiConfig.flags.crOnVariants;
const stickiness = variants[0]?.stickiness || defaultStickiness;
const stickiness = useMemo(() => {
if (!loading) {
return variants[0]?.stickiness || defaultStickiness;
}
return '';
}, [loading, defaultStickiness, JSON.stringify(variants[0] ?? {})]);
const stickinessOptions = useMemo(
() => [
'default',
@ -296,7 +302,7 @@ export const EnvironmentVariantsModal = ({
};
const onStickinessChange = (value: string) => {
updateStickiness(value).catch(console.warn);
updateStickiness(value);
};
const [error, setError] = useState<string | undefined>();
@ -308,14 +314,13 @@ export const EnvironmentVariantsModal = ({
}, [apiPayload.error]);
const handleClose = () => {
updateStickiness(defaultStickiness).then();
updateStickiness(defaultStickiness);
setOpen(false);
};
if (loading || stickiness === '') {
return <Loader />;
}
return (
<SidebarModal open={open} onClose={handleClose} label="">
<FormTemplate

View File

@ -12,10 +12,10 @@ import {
parseParameterString,
} from 'utils/parseParameter';
import { StickinessSelect } from './StickinessSelect/StickinessSelect';
import { useOptionalPathParam } from 'hooks/useOptionalPathParam';
import { useDefaultProjectSettings } from 'hooks/useDefaultProjectSettings';
import Loader from '../../../common/Loader/Loader';
import { useMemo } from 'react';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
interface IFlexibleStrategyProps {
parameters: IFeatureStrategyParameters;
@ -29,7 +29,7 @@ const FlexibleStrategy = ({
parameters,
editable = true,
}: IFlexibleStrategyProps) => {
const projectId = useOptionalPathParam('projectId');
const projectId = useRequiredPathParam('projectId');
const { defaultStickiness, loading } = useDefaultProjectSettings(projectId);
const onUpdate = (field: string) => (newValue: string) => {

View File

@ -41,7 +41,6 @@ export const StickinessSelect = ({
);
const stickinessOptions = resolveStickinessOptions();
return (
<Select
id="stickiness-select"

View File

@ -13,6 +13,7 @@ const fallbackProject: IProject = {
description: 'Default',
favorite: false,
mode: 'open',
defaultStickiness: 'default',
stats: {
archivedCurrentWindow: 0,
archivedPastWindow: 0,

View File

@ -1,57 +1,19 @@
import useUiConfig from './api/getters/useUiConfig/useUiConfig';
import { SWRConfiguration } from 'swr';
import { useCallback } from 'react';
import handleErrorResponses from './api/getters/httpErrorResponseHandler';
import { useConditionalSWR } from './api/getters/useConditionalSWR/useConditionalSWR';
import { ProjectMode } from 'component/project/Project/hooks/useProjectForm';
import { formatApiPath } from 'utils/formatPath';
import useProject from './api/getters/useProject/useProject';
export interface ISettingsResponse {
defaultStickiness?: string;
mode?: ProjectMode;
}
const DEFAULT_STICKINESS = 'default';
export const useDefaultProjectSettings = (
projectId?: string,
options?: SWRConfiguration
) => {
export const useDefaultProjectSettings = (projectId: string) => {
const { uiConfig } = useUiConfig();
const PATH = `api/admin/projects/${projectId}/settings`;
const { projectScopedStickiness } = uiConfig.flags;
const { data, isLoading, error, mutate } =
useConditionalSWR<ISettingsResponse>(
Boolean(projectId) && Boolean(projectScopedStickiness),
{},
['useDefaultProjectSettings', PATH],
() => fetcher(formatApiPath(PATH)),
options
);
const defaultStickiness = (): string => {
if (!isLoading) {
if (data?.defaultStickiness) {
return data?.defaultStickiness;
}
return DEFAULT_STICKINESS;
}
return '';
};
const refetch = useCallback(() => {
mutate().catch(console.warn);
}, [mutate]);
const { project, loading, error } = useProject(projectId);
return {
defaultStickiness: defaultStickiness(),
refetch,
loading: isLoading,
defaultStickiness: Boolean(projectScopedStickiness)
? project.defaultStickiness
: DEFAULT_STICKINESS,
mode: project.mode,
loading: loading,
error,
};
};
const fetcher = (path: string) => {
return fetch(path)
.then(handleErrorResponses('Project stickiness data'))
.then(res => res.json());
};

View File

@ -24,6 +24,7 @@ export interface IProject {
favorite: boolean;
features: IFeatureToggleListItem[];
mode: 'open' | 'protected';
defaultStickiness: string;
}
export interface IProjectHealthReport extends IProject {

View File

@ -33,7 +33,7 @@ const COLUMNS = [
'updated_at',
];
const TABLE = 'projects';
const SETTINGS_COLUMNS = ['project_mode'];
const SETTINGS_COLUMNS = ['project_mode', 'default_stickiness'];
const SETTINGS_TABLE = 'project_settings';
export interface IEnvironmentProjectLink {
@ -531,6 +531,7 @@ class ProjectStore implements IProjectStore {
health: row.health ?? 100,
updatedAt: row.updated_at || new Date(),
mode: row.project_mode || 'open',
defaultStickiness: row.default_stickiness || 'default',
};
}
}

View File

@ -798,13 +798,12 @@ export default class ProjectService {
: Promise.resolve(false),
this.projectStatsStore.getProjectStats(projectId),
]);
return {
stats: projectStats,
name: project.name,
description: project.description,
mode: project.mode,
defaultStickiness: project.defaultStickiness || 'default',
defaultStickiness: project.defaultStickiness,
health: project.health || 0,
favorite: favorite,
updatedAt: project.updatedAt,

View File

@ -372,7 +372,7 @@ export interface IProject {
updatedAt?: Date;
changeRequestsEnabled?: boolean;
mode: ProjectMode;
defaultStickiness?: string;
defaultStickiness: string;
}
export interface ICustomRole {

View File

@ -525,7 +525,12 @@ describe('Interacting with features using project IDs that belong to other proje
rootRole: RoleName.ADMIN,
});
await app.services.projectService.createProject(
{ name: otherProject, id: otherProject, mode: 'open' },
{
name: otherProject,
id: otherProject,
mode: 'open',
defaultStickiness: 'clientId',
},
dummyAdmin,
);

View File

@ -101,7 +101,7 @@ const createProject = async (id: string, name: string): Promise<void> => {
email: `${randomId()}@example.com`,
});
await app.services.projectService.createProject(
{ id, name, mode: 'open' },
{ id, name, mode: 'open', defaultStickiness: 'default' },
user,
);
};

View File

@ -43,6 +43,7 @@ beforeAll(async () => {
name: 'Test Project',
description: 'Fancy',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
const user = await stores.userStore.insert({
name: 'Some Name',

View File

@ -107,6 +107,7 @@ test('should list all projects', async () => {
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'default',
};
await projectService.createProject(project, user);
@ -121,6 +122,7 @@ test('should create new project', async () => {
name: 'New project',
description: 'Blah',
mode: 'protected' as const,
defaultStickiness: 'default',
};
await projectService.createProject(project, user);
@ -138,6 +140,7 @@ test('should delete project', async () => {
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'default',
};
await projectService.createProject(project, user);
@ -156,12 +159,14 @@ test('should not be able to delete project with toggles', async () => {
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
await projectService.createProject(project, user);
await stores.featureToggleStore.create(project.id, {
name: 'test-project-delete',
project: project.id,
enabled: false,
defaultStickiness: 'default',
});
try {
@ -192,6 +197,7 @@ test('should not be able to create existing project', async () => {
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'default',
};
try {
await projectService.createProject(project, user);
@ -223,6 +229,7 @@ test('should update project', async () => {
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'default',
};
const updatedProject = {
@ -230,6 +237,7 @@ test('should update project', async () => {
name: 'New name',
description: 'Blah longer desc',
mode: 'protected' as const,
defaultStickiness: 'userId',
};
await projectService.createProject(project, user);
@ -240,6 +248,7 @@ test('should update project', async () => {
expect(updatedProject.name).toBe(readProject.name);
expect(updatedProject.description).toBe(readProject.description);
expect(updatedProject.mode).toBe('protected');
expect(updatedProject.defaultStickiness).toBe('userId');
});
test('should update project without existing settings', async () => {
@ -248,6 +257,7 @@ test('should update project without existing settings', async () => {
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'default',
};
const updatedProject = {
@ -255,6 +265,7 @@ test('should update project without existing settings', async () => {
name: 'New name',
description: 'Blah longer desc',
mode: 'protected' as const,
defaultStickiness: 'clientId',
};
await projectService.createProject(project, user);
@ -269,6 +280,7 @@ test('should update project without existing settings', async () => {
expect(updatedProject.name).toBe(readProject.name);
expect(updatedProject.description).toBe(readProject.description);
expect(updatedProject.mode).toBe('protected');
expect(updatedProject.defaultStickiness).toBe('clientId');
});
test('should give error when getting unknown project', async () => {
@ -285,6 +297,7 @@ test('should get list of users with access to project', async () => {
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
await projectService.createProject(project, user);
const { users } = await projectService.getAccessToProject(project.id);
@ -307,6 +320,7 @@ test('should add a member user to the project', async () => {
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
await projectService.createProject(project, user);
@ -363,6 +377,7 @@ test('should add admin users to the project', async () => {
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
await projectService.createProject(project, user);
@ -410,6 +425,7 @@ test('add user should fail if user already have access', async () => {
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
await projectService.createProject(project, user);
@ -445,6 +461,7 @@ test('should remove user from the project', async () => {
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
await projectService.createProject(project, user);
@ -480,6 +497,7 @@ test('should not remove user from the project', async () => {
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
await projectService.createProject(project, user);
@ -504,6 +522,7 @@ test('should not change project if feature toggle project does not match current
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
const toggle = { name: 'test-toggle' };
@ -531,6 +550,7 @@ test('should return 404 if no project is found with the project id', async () =>
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
const toggle = { name: 'test-toggle-2' };
@ -556,6 +576,7 @@ test('should fail if user is not authorized', async () => {
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
const projectDestination = {
@ -563,6 +584,7 @@ test('should fail if user is not authorized', async () => {
name: 'New project 2',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
const toggle = { name: 'test-toggle-3' };
@ -594,11 +616,13 @@ test('should change project when checks pass', async () => {
id: randomId(),
name: randomId(),
mode: 'open' as const,
defaultStickiness: 'clientId',
};
const projectB = {
id: randomId(),
name: randomId(),
mode: 'open' as const,
defaultStickiness: 'clientId',
};
const toggle = { name: randomId() };
@ -623,11 +647,13 @@ test('changing project should emit event even if user does not have a username s
id: randomId(),
name: randomId(),
mode: 'open' as const,
defaultStickiness: 'default',
};
const projectB = {
id: randomId(),
name: randomId(),
mode: 'open' as const,
defaultStickiness: 'clientId',
};
const toggle = { name: randomId() };
await projectService.createProject(projectA, user);
@ -649,11 +675,13 @@ test('should require equal project environments to move features', async () => {
id: randomId(),
name: randomId(),
mode: 'open' as const,
defaultStickiness: 'clientId',
};
const projectB = {
id: randomId(),
name: randomId(),
mode: 'open' as const,
defaultStickiness: 'clientId',
};
const environment = { name: randomId(), type: 'production' };
const toggle = { name: randomId() };
@ -683,6 +711,7 @@ test('A newly created project only gets connected to enabled environments', asyn
name: 'New environment project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
const enabledEnv = 'connection_test';
await db.stores.environmentStore.create({
@ -710,6 +739,7 @@ test('should have environments sorted in order', async () => {
name: 'Environment testing project',
description: '',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
const first = 'test';
const second = 'abc';
@ -749,6 +779,7 @@ test('should add a user to the project with a custom role', async () => {
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
await projectService.createProject(project, user);
@ -800,6 +831,7 @@ test('should delete role entries when deleting project', async () => {
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
await projectService.createProject(project, user);
@ -852,6 +884,7 @@ test('should change a users role in the project', async () => {
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
await projectService.createProject(project, user);
@ -919,6 +952,7 @@ test('should update role for user on project', async () => {
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
await projectService.createProject(project, user);
@ -957,6 +991,7 @@ test('should able to assign role without existing members', async () => {
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
await projectService.createProject(project, user);
@ -1000,6 +1035,7 @@ test('should not update role for user on project when she is the owner', async (
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
await projectService.createProject(project, user);
@ -1034,6 +1070,7 @@ test('Should allow bulk update of group permissions', async () => {
id: 'bulk-update-project',
name: 'bulk-update-project',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
await projectService.createProject(project, user.id);
const groupStore = stores.groupStore;
@ -1111,6 +1148,7 @@ test('Should allow bulk update of only groups', async () => {
id: 'bulk-update-project-only',
name: 'bulk-update-project-only',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
const groupStore = stores.groupStore;
@ -1152,6 +1190,7 @@ test('should only count active feature toggles for project', async () => {
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
await projectService.createProject(project, user);
@ -1180,6 +1219,7 @@ test('should list projects with all features archived', async () => {
name: 'Listed project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
await projectService.createProject(project, user);
@ -1216,6 +1256,7 @@ test('should calculate average time to production', async () => {
id: 'average-time-to-prod',
name: 'average-time-to-prod',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
await projectService.createProject(project, user.id);
@ -1274,6 +1315,7 @@ test('should calculate average time to production ignoring some items', async ()
id: 'average-time-to-prod-corner-cases',
name: 'average-time-to-prod',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
const makeEvent = (featureName: string) => ({
enabled: true,
@ -1362,6 +1404,7 @@ test('should get correct amount of features created in current and past window',
id: 'features-created',
name: 'features-created',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
await projectService.createProject(project, user.id);
@ -1398,6 +1441,7 @@ test('should get correct amount of features archived in current and past window'
id: 'features-archived',
name: 'features-archived',
mode: 'open' as const,
defaultStickiness: 'clientId',
};
await projectService.createProject(project, user.id);
@ -1448,6 +1492,7 @@ test('should get correct amount of project members for current and past window',
id: 'features-members',
name: 'features-members',
mode: 'open' as const,
defaultStickiness: 'default',
};
await projectService.createProject(project, user.id);

View File

@ -55,6 +55,7 @@ export default class FakeProjectStore implements IProjectStore {
health: 100,
createdAt: new Date(),
mode: 'open',
defaultStickiness: 'default',
};
this.projects.push(newProj);
return newProj;