1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-20 00:08:02 +01:00

feat: implement project-scoped segments in project settings (#3335)

https://linear.app/unleash/issue/2-743/have-a-project-specific-configuration-section


![image](https://user-images.githubusercontent.com/14320932/225657038-1a385e6e-deb3-4229-a30d-e7ca28ef2b3c.png)

Adds the "segments" option to project settings, providing the usual CRUD
operations but scoped to the specific project.
This commit is contained in:
Nuno Góis 2023-03-17 08:23:27 +00:00 committed by GitHub
parent f685c1059f
commit 292d6a7f60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 140 additions and 17 deletions

View File

@ -34,6 +34,7 @@ const StyledContainer = styled('section', {
minHeight: modal ? '100vh' : '80vh',
borderRadius: modal ? 0 : theme.spacing(2),
width: '100%',
height: '100%',
display: 'flex',
margin: '0 auto',
[theme.breakpoints.down(1100)]: {

View File

@ -46,6 +46,7 @@ const StyledHeader = styled('div')(({ theme }) => ({
const StyledHeaderTitle = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.mainHeader,
fontWeight: 'normal',
lineHeight: theme.spacing(5),
}));
const StyledHeaderActions = styled('div')(({ theme }) => ({

View File

@ -63,6 +63,11 @@ const PremiumFeatures = {
url: 'https://docs.getunleash.io/reference/change-requests',
label: 'Change Requests',
},
segments: {
plan: FeaturePlan.PRO,
url: 'https://docs.getunleash.io/reference/segments',
label: 'Segments',
},
};
type PremiumFeatureType = keyof typeof PremiumFeatures;

View File

@ -0,0 +1,63 @@
import { PageContent } from 'component/common/PageContent/PageContent';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { usePageTitle } from 'hooks/usePageTitle';
import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject';
import { SegmentTable } from 'component/segments/SegmentTable';
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
import { Route, Routes, useNavigate } from 'react-router-dom';
import { CreateSegment } from 'component/segments/CreateSegment/CreateSegment';
import { EditSegment } from 'component/segments/EditSegment/EditSegment';
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
import { GO_BACK } from 'constants/navigate';
export const ProjectSegments = () => {
const projectId = useRequiredPathParam('projectId');
const projectName = useProjectNameOrId(projectId);
const { isOss } = useUiConfig();
const navigate = useNavigate();
usePageTitle(`Project segments ${projectName}`);
if (isOss()) {
return (
<PageContent
header={<PageHeader titleElement="Segments" />}
sx={{ justifyContent: 'center' }}
>
<PremiumFeature feature="segments" />
</PageContent>
);
}
return (
<Routes>
<Route
path="create"
element={
<SidebarModal
open
onClose={() => navigate(GO_BACK)}
label="Create segment"
>
<CreateSegment />
</SidebarModal>
}
/>
<Route
path="edit/:segmentId"
element={
<SidebarModal
open
onClose={() => navigate(GO_BACK)}
label="Edit segment"
>
<EditSegment />
</SidebarModal>
}
/>
<Route path="*" element={<SegmentTable />} />
</Routes>
);
};

View File

@ -11,12 +11,13 @@ import ProjectEnvironmentList from 'component/project/ProjectEnvironment/Project
import { ChangeRequestConfiguration } from './ChangeRequestConfiguration/ChangeRequestConfiguration';
import { ProjectApiAccess } from 'component/project/Project/ProjectSettings/ProjectApiAccess/ProjectApiAccess';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
import { ProjectSegments } from './ProjectSegments/ProjectSegments';
export const ProjectSettings = () => {
const location = useLocation();
const navigate = useNavigate();
const { uiConfig } = useUiConfig();
const { showProjectApiAccess } = uiConfig.flags;
const { showProjectApiAccess, projectScopedSegments } = uiConfig.flags;
const tabs: ITab[] = [
{
@ -27,6 +28,11 @@ export const ProjectSettings = () => {
id: 'access',
label: 'Access',
},
{
id: 'segments',
label: 'Segments',
hidden: !Boolean(projectScopedSegments),
},
{
id: 'change-requests',
label: 'Change request configuration',
@ -60,6 +66,7 @@ export const ProjectSettings = () => {
element={<ProjectEnvironmentList />}
/>
<Route path="access/*" element={<ProjectAccess />} />
<Route path="segments/*" element={<ProjectSegments />} />
<Route
path="change-requests/*"
element={<ChangeRequestConfiguration />}

View File

@ -16,8 +16,10 @@ import { segmentsDocsLink } from 'component/segments/SegmentDocs';
import { useSegmentValuesCount } from 'component/segments/hooks/useSegmentValuesCount';
import { SEGMENT_CREATE_BTN_ID } from 'utils/testIds';
import { useSegmentLimits } from 'hooks/api/getters/useSegmentLimits/useSegmentLimits';
import { useOptionalPathParam } from 'hooks/useOptionalPathParam';
export const CreateSegment = () => {
const projectId = useOptionalPathParam('projectId');
const { uiConfig } = useUiConfig();
const { setToastData, setToastApiError } = useToast();
const { showFeedbackCES } = useContext(feedbackCESContext);
@ -37,7 +39,7 @@ export const CreateSegment = () => {
getSegmentPayload,
errors,
clearErrors,
} = useSegmentForm();
} = useSegmentForm('', '', projectId);
const hasValidConstraints = useConstraintsValidation(constraints);
const { segmentValuesLimit } = useSegmentLimits();
@ -62,7 +64,11 @@ export const CreateSegment = () => {
try {
await createSegment(getSegmentPayload());
await refetchSegments();
navigate('/segments/');
if (projectId) {
navigate(`/projects/${projectId}/settings/segments/`);
} else {
navigate('/segments/');
}
setToastData({
title: 'Segment created',
confetti: true,

View File

@ -2,13 +2,21 @@ import { CREATE_SEGMENT } from 'component/providers/AccessProvider/permissions';
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
import { NAVIGATE_TO_CREATE_SEGMENT } from 'utils/testIds';
import { useNavigate } from 'react-router-dom';
import { useOptionalPathParam } from 'hooks/useOptionalPathParam';
export const CreateSegmentButton = () => {
const projectId = useOptionalPathParam('projectId');
const navigate = useNavigate();
return (
<PermissionButton
onClick={() => navigate('/segments/create')}
onClick={() => {
if (projectId) {
navigate(`/projects/${projectId}/settings/segments/create`);
} else {
navigate('/segments/create');
}
}}
permission={CREATE_SEGMENT}
data-testid={NAVIGATE_TO_CREATE_SEGMENT}
>

View File

@ -18,8 +18,10 @@ import { segmentsDocsLink } from 'component/segments/SegmentDocs';
import { useSegmentValuesCount } from 'component/segments/hooks/useSegmentValuesCount';
import { SEGMENT_SAVE_BTN_ID } from 'utils/testIds';
import { useSegmentLimits } from 'hooks/api/getters/useSegmentLimits/useSegmentLimits';
import { useOptionalPathParam } from 'hooks/useOptionalPathParam';
export const EditSegment = () => {
const projectId = useOptionalPathParam('projectId');
const segmentId = useRequiredPathParam('segmentId');
const { segment } = useSegment(Number(segmentId));
const { uiConfig } = useUiConfig();
@ -71,7 +73,11 @@ export const EditSegment = () => {
try {
await updateSegment(segment.id, getSegmentPayload());
await refetchSegments();
navigate('/segments/');
if (projectId) {
navigate(`/projects/${projectId}/settings/segments/`);
} else {
navigate('/segments/');
}
setToastData({
title: 'Segment updated',
type: 'success',

View File

@ -3,17 +3,27 @@ import { Edit } from '@mui/icons-material';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { UPDATE_SEGMENT } from 'component/providers/AccessProvider/permissions';
import { useNavigate } from 'react-router-dom';
import { useOptionalPathParam } from 'hooks/useOptionalPathParam';
interface IEditSegmentButtonProps {
segment: ISegment;
}
export const EditSegmentButton = ({ segment }: IEditSegmentButtonProps) => {
const projectId = useOptionalPathParam('projectId');
const navigate = useNavigate();
return (
<PermissionIconButton
onClick={() => navigate(`/segments/edit/${segment.id}`)}
onClick={() => {
if (projectId) {
navigate(
`/projects/${projectId}/settings/segments/edit/${segment.id}`
);
} else {
navigate(`/segments/edit/${segment.id}`);
}
}}
permission={UPDATE_SEGMENT}
tooltipProps={{ title: 'Edit segment' }}
>

View File

@ -11,6 +11,8 @@ import {
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useProjects from 'hooks/api/getters/useProjects/useProjects';
import { useOptionalPathParam } from 'hooks/useOptionalPathParam';
import { GO_BACK } from 'constants/navigate';
interface ISegmentFormPartOneProps {
name: string;
@ -65,6 +67,7 @@ export const SegmentFormStepOne: React.FC<ISegmentFormPartOneProps> = ({
clearErrors,
setCurrentStep,
}) => {
const projectId = useOptionalPathParam('projectId');
const { uiConfig } = useUiConfig();
const navigate = useNavigate();
const { projects } = useProjects();
@ -105,7 +108,10 @@ export const SegmentFormStepOne: React.FC<ISegmentFormPartOneProps> = ({
data-testid={SEGMENT_DESC_ID}
/>
<ConditionallyRender
condition={Boolean(uiConfig.flags.projectScopedSegments)}
condition={
Boolean(uiConfig.flags.projectScopedSegments) &&
!projectId
}
show={
<>
<StyledInputDescription>
@ -141,7 +147,7 @@ export const SegmentFormStepOne: React.FC<ISegmentFormPartOneProps> = ({
<StyledCancelButton
type="button"
onClick={() => {
navigate('/segments');
navigate(GO_BACK);
}}
>
Cancel

View File

@ -29,6 +29,7 @@ import {
import { useSegmentValuesCount } from 'component/segments/hooks/useSegmentValuesCount';
import AccessContext from 'contexts/AccessContext';
import { useSegmentLimits } from 'hooks/api/getters/useSegmentLimits/useSegmentLimits';
import { GO_BACK } from 'constants/navigate';
interface ISegmentFormPartTwoProps {
constraints: IConstraint[];
@ -214,7 +215,7 @@ export const SegmentFormStepTwo: React.FC<ISegmentFormPartTwoProps> = ({
<StyledCancelButton
type="button"
onClick={() => {
navigate('/segments');
navigate(GO_BACK);
}}
>
Cancel

View File

@ -28,8 +28,10 @@ import { Search } from 'component/common/Search/Search';
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useOptionalPathParam } from 'hooks/useOptionalPathParam';
export const SegmentTable = () => {
const projectId = useOptionalPathParam('projectId');
const { segments, loading } = useSegments();
const { uiConfig } = useUiConfig();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
@ -39,17 +41,22 @@ export const SegmentTable = () => {
});
const data = useMemo(() => {
return (
segments ??
Array(5).fill({
if (!segments) {
return Array(5).fill({
name: 'Segment name',
description: 'Segment descripton',
createdAt: new Date().toISOString(),
createdBy: 'user',
projectId: 'Project',
})
);
}, [segments]);
});
}
if (projectId) {
return segments.filter(({ project }) => project === projectId);
}
return segments;
}, [segments, projectId]);
const {
getTableProps,
@ -85,7 +92,9 @@ export const SegmentTable = () => {
columns: ['createdAt', 'createdBy'],
},
{
condition: !Boolean(uiConfig.flags.projectScopedSegments),
condition:
Boolean(projectId) ||
!Boolean(uiConfig.flags.projectScopedSegments),
columns: ['project'],
},
],

View File

@ -23,7 +23,7 @@ exports[`renders an empty list correctly 1`] = `
data-loading={true}
>
<h1
className="MuiTypography-root MuiTypography-h1 css-1pr8obe-MuiTypography-root"
className="MuiTypography-root MuiTypography-h1 css-1jqnoga-MuiTypography-root"
>
Tag types (5)
</h1>