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:
parent
f685c1059f
commit
292d6a7f60
@ -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)]: {
|
||||
|
@ -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 }) => ({
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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 />}
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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',
|
||||
|
@ -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' }}
|
||||
>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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'],
|
||||
},
|
||||
],
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user