1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-03 01:18:43 +02:00

Feat/add enterprise badge to change req settings (#2585)

<!-- 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! ❤️ -->
Disable change requests for Pro and oss
## 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: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com>
Co-authored-by: Nuno Góis <github@nunogois.com>
This commit is contained in:
andreas-unleash 2022-12-06 10:05:49 +02:00 committed by GitHub
parent cfc347f1b6
commit 7af155fe64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 551 additions and 393 deletions

View File

@ -0,0 +1,86 @@
import { ReactComponent as ProPlanIcon } from 'assets/icons/pro-enterprise-feature-badge.svg';
import { Box, Link, styled, Typography } from '@mui/material';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
const PremiumFeatureWrapper = styled(Box)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
padding: theme.spacing(1, 0.5),
}));
const StyledTitle = styled(Typography)(({ theme }) => ({
display: 'inline-flex',
alignItems: 'center',
fontWeight: theme.fontWeight.bold,
fontSize: theme.fontSizes.smallBody,
gap: theme.spacing(1),
}));
const StyledBody = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
margin: theme.spacing(1, 0),
}));
const StyledLink = styled(Link)(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
width: 'fit-content',
}));
enum FeatureLevelTitle {
PRO = 'Pro & Enterprise feature',
ENTERPRISE = 'Enterprise feature',
}
export enum PlausibleOrigin {
PROJECT = 'Projects',
ACCESS = 'Access',
CHANGE_REQUEST = 'Change Request',
}
export interface PremiumFeatureProps {
children: React.ReactNode;
origin?: PlausibleOrigin;
center?: boolean;
enterpriseOnly?: boolean;
}
export const PremiumFeature = ({
children,
origin,
center,
enterpriseOnly = false,
}: PremiumFeatureProps) => {
const tracker = usePlausibleTracker();
const handleClick = () => {
if (origin) {
tracker.trackEvent('upgrade_plan_clicked', {
props: { origin },
});
}
};
return (
<PremiumFeatureWrapper
sx={{
alignItems: center ? 'center' : 'start',
textAlign: center ? 'center' : 'left',
}}
>
<StyledTitle>
<ProPlanIcon />
{enterpriseOnly
? FeatureLevelTitle.ENTERPRISE
: FeatureLevelTitle.PRO}
</StyledTitle>
<StyledBody>{children}</StyledBody>
<StyledLink
href={'https://www.getunleash.io/plans'}
target="_blank"
onClick={handleClick}
>
Upgrade now
</StyledLink>
</PremiumFeatureWrapper>
);
};

View File

@ -1,48 +0,0 @@
import { ReactComponent as ProPlanIcon } from 'assets/icons/pro-enterprise-feature-badge.svg';
import { Box, Link, styled, Typography } from '@mui/material';
export interface ProFeatureTooltipProps {
children: React.ReactNode;
}
const ProFeatureTooltipWrapper = styled(Box)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
padding: theme.spacing(1, 0.5),
}));
const StyledTitle = styled(Typography)(({ theme }) => ({
display: 'inline-flex',
alignItems: 'center',
fontWeight: theme.fontWeight.bold,
fontSize: theme.fontSizes.smallBody,
gap: theme.spacing(1),
}));
const StyledBody = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
margin: theme.spacing(1, 0),
}));
const StyledLink = styled(Link)(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
width: 'fit-content',
}));
export const ProFeatureTooltip = ({ children }: ProFeatureTooltipProps) => {
return (
<ProFeatureTooltipWrapper>
<StyledTitle>
<ProPlanIcon />
Pro & Enterprise feature
</StyledTitle>
<StyledBody>{children}</StyledBody>
<StyledLink
href={'https://www.getunleash.io/plans'}
target="_blank"
>
Upgrade now
</StyledLink>
</ProFeatureTooltipWrapper>
);
};

View File

@ -1,25 +1,36 @@
import { styled } from '@mui/material';
import { Button, styled } from '@mui/material';
const StyledTab = styled('button')<{ selected: boolean }>(
const StyledTab = styled(Button)<{ selected: boolean }>(
({ theme, selected }) => ({
cursor: 'pointer',
border: 0,
backgroundColor: selected
? theme.palette.background.paper
: 'transparent',
borderLeft: `${theme.spacing(1)} solid ${
selected ? theme.palette.primary.main : 'transparent'
}`,
borderRadius: theme.shape.borderRadiusMedium,
padding: theme.spacing(2, 4),
color: theme.palette.text.primary,
fontSize: theme.fontSizes.bodySize,
fontWeight: selected ? theme.fontWeight.bold : theme.fontWeight.medium,
textAlign: 'left',
transition: 'background-color 0.2s ease',
'&.MuiButton-root': {
cursor: 'pointer',
height: theme.spacing(6.5),
border: 0,
backgroundColor: selected
? theme.palette.background.paper
: 'transparent',
borderLeft: `${theme.spacing(1)} solid ${
selected ? theme.palette.primary.main : 'transparent'
}`,
borderRadius: theme.shape.borderRadiusMedium,
justifyContent: 'start',
transition: 'background-color 0.2s ease',
color: theme.palette.text.primary,
textAlign: 'left',
padding: theme.spacing(2, 4),
fontSize: theme.fontSizes.bodySize,
fontWeight: selected
? theme.fontWeight.bold
: theme.fontWeight.medium,
lineHeight: 1.2,
},
'&:hover': {
backgroundColor: theme.palette.neutral.light,
},
'&.Mui-disabled': {
pointerEvents: 'auto',
},
justifyContent: 'space-between',
})
);
@ -34,7 +45,15 @@ export const VerticalTab = ({
selected,
onClick,
}: IVerticalTabProps) => (
<StyledTab selected={Boolean(selected)} onClick={onClick}>
<StyledTab
selected={Boolean(selected)}
onClick={onClick}
disableRipple
disableElevation
disableFocusRipple
disableTouchRipple
fullWidth
>
{label}
</StyledTab>
);

View File

@ -1,313 +1,70 @@
import React, { useContext, useMemo, useState, VFC } from 'react';
import { HeaderGroup, useGlobalFilter, useTable } from 'react-table';
import { Alert, Box, styled, Typography } from '@mui/material';
import {
SortableTableHeader,
Table,
TableBody,
TableCell,
TableRow,
} from 'component/common/Table';
import { sortTypes } from 'utils/sortTypes';
import { useContext } from 'react';
import { PageContent } from 'component/common/PageContent/PageContent';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { Alert, Link, styled } from '@mui/material';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import PermissionSwitch from 'component/common/PermissionSwitch/PermissionSwitch';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useChangeRequestConfig } from 'hooks/api/getters/useChangeRequestConfig/useChangeRequestConfig';
import {
IChangeRequestConfig,
useChangeRequestApi,
} from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
import { UPDATE_PROJECT } from '@server/types/permissions';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { ChangeRequestProcessHelp } from './ChangeRequestProcessHelp/ChangeRequestProcessHelp';
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import { KeyboardArrowDownOutlined } from '@mui/icons-material';
import { useTheme } from '@mui/material/styles';
import AccessContext from 'contexts/AccessContext';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { usePageTitle } from 'hooks/usePageTitle';
import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject';
import { ChangeRequestTable } from './ChangeRequestTable';
import {
PlausibleOrigin,
PremiumFeature,
} from 'component/common/PremiumFeature/PremiumFeature';
const StyledBox = styled(Box)(({ theme }) => ({
padding: theme.spacing(1),
display: 'flex',
justifyContent: 'center',
'& .MuiInputBase-input': {
fontSize: theme.fontSizes.smallBody,
},
const StyledLink = styled(Link)(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
width: 'fit-content',
}));
export const ChangeRequestConfiguration: VFC = () => {
const { trackEvent } = usePlausibleTracker();
const [dialogState, setDialogState] = useState<{
isOpen: boolean;
enableEnvironment: string;
isEnabled: boolean;
requiredApprovals: number;
}>({
isOpen: false,
enableEnvironment: '',
isEnabled: false,
requiredApprovals: 1,
});
const theme = useTheme();
export const ChangeRequestConfiguration = () => {
const projectId = useRequiredPathParam('projectId');
const { data, loading, refetchChangeRequestConfig } =
useChangeRequestConfig(projectId);
const { updateChangeRequestEnvironmentConfig } = useChangeRequestApi();
const { setToastData, setToastApiError } = useToast();
const onRowChange =
(
enableEnvironment: string,
isEnabled: boolean,
requiredApprovals: number
) =>
() => {
setDialogState({
isOpen: true,
enableEnvironment,
isEnabled,
requiredApprovals,
});
};
const onConfirm = async () => {
if (dialogState.enableEnvironment) {
await updateConfiguration();
}
setDialogState(state => ({ ...state, isOpen: false }));
};
async function updateConfiguration(config?: IChangeRequestConfig) {
try {
await updateChangeRequestEnvironmentConfig(
config || {
project: projectId,
environment: dialogState.enableEnvironment,
enabled: !dialogState.isEnabled,
requiredApprovals: dialogState.requiredApprovals,
}
);
setToastData({
type: 'success',
title: 'Updated change request status',
text: 'Successfully updated change request status.',
});
await refetchChangeRequestConfig();
} catch (error) {
setToastApiError(formatUnknownError(error));
}
}
const approvalOptions = Array.from(Array(10).keys())
.map(key => String(key + 1))
.map(key => {
const labelText = key === '1' ? 'approval' : 'approvals';
return {
key,
label: `${key} ${labelText}`,
sx: { 'font-size': theme.fontSizes.smallBody },
};
});
function onRequiredApprovalsChange(original: any, approvals: string) {
updateConfiguration({
project: projectId,
environment: original.environment,
enabled: original.changeRequestEnabled,
requiredApprovals: Number(approvals),
});
}
const columns = useMemo(
() => [
{
Header: 'Environment',
accessor: 'environment',
disableSortBy: true,
},
{
Header: 'Type',
accessor: 'type',
disableGlobalFilter: true,
disableSortBy: true,
},
{
Header: 'Required approvals',
align: 'center',
Cell: ({ row: { original } }: any) => {
const { hasAccess } = useContext(AccessContext);
return (
<ConditionallyRender
condition={original.changeRequestEnabled}
show={
<StyledBox data-loading>
<GeneralSelect
options={approvalOptions}
value={original.requiredApprovals || 1}
onChange={approvals => {
onRequiredApprovalsChange(
original,
approvals
);
}}
disabled={
!hasAccess(
UPDATE_PROJECT,
projectId
)
}
IconComponent={
KeyboardArrowDownOutlined
}
fullWidth
/>
</StyledBox>
}
/>
);
},
width: 100,
disableGlobalFilter: true,
disableSortBy: true,
},
{
Header: 'Status',
accessor: 'changeRequestEnabled',
id: 'changeRequestEnabled',
align: 'center',
Cell: ({ value, row: { original } }: any) => (
<StyledBox data-loading>
<PermissionSwitch
checked={value}
projectId={projectId}
permission={UPDATE_PROJECT}
inputProps={{ 'aria-label': original.environment }}
onClick={onRowChange(
original.environment,
original.changeRequestEnabled,
original.requiredApprovals
)}
/>
</StyledBox>
),
width: 100,
disableGlobalFilter: true,
disableSortBy: true,
},
],
[]
const projectName = useProjectNameOrId(projectId);
const { hasAccess } = useContext(AccessContext);
const { isOss, uiConfig } = useUiConfig();
const isPro = !(
Boolean(uiConfig.versionInfo?.current.oss) ||
Boolean(uiConfig.versionInfo?.current.enterprise)
);
usePageTitle(`Project change request ${projectName}`);
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
useTable(
{
// @ts-ignore
columns,
data,
sortTypes,
autoResetGlobalFilter: false,
disableSortRemove: true,
defaultColumn: {
Cell: TextCell,
},
},
useGlobalFilter
);
return (
<PageContent
header={
<PageHeader
titleElement="Change request configuration"
actions={<ChangeRequestProcessHelp />}
/>
}
isLoading={loading}
>
<Alert severity="info" sx={{ mb: 3 }}>
If change request is enabled for an environment, then any change
in that environment needs to be approved before it will be
applied
</Alert>
<Table {...getTableProps()}>
<SortableTableHeader
headerGroups={headerGroups as HeaderGroup<object>[]}
/>
<TableBody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row);
return (
<TableRow hover {...row.getRowProps()}>
{row.cells.map(cell => (
<TableCell {...cell.getCellProps()}>
{cell.render('Cell')}
</TableCell>
))}
</TableRow>
);
})}
</TableBody>
</Table>
<Dialogue
onClick={() => {
trackEvent('change_request', {
props: {
eventType: `change request ${
!dialogState.isEnabled ? 'enabled' : 'disabled'
}`,
},
});
onConfirm();
}}
open={dialogState.isOpen}
onClose={() =>
setDialogState(state => ({ ...state, isOpen: false }))
}
primaryButtonText={dialogState.isEnabled ? 'Disable' : 'Enable'}
secondaryButtonText="Cancel"
title={`${
dialogState.isEnabled ? 'Disable' : 'Enable'
} change requests`}
if (isOss() || isPro) {
return (
<PageContent
header={<PageHeader title="Change request configuration" />}
sx={{ justifyContent: 'center' }}
>
<Typography sx={{ mb: 1 }}>
You are about to{' '}
{dialogState.isEnabled ? 'disable' : 'enable'} Change
request
<ConditionallyRender
condition={Boolean(dialogState.enableEnvironment)}
show={
<>
{' '}
for{' '}
<strong>{dialogState.enableEnvironment}</strong>
</>
}
/>
.
</Typography>
<ConditionallyRender
condition={!dialogState.isEnabled}
show={
<Typography variant="body2" color="text.secondary">
When enabling change request for an environment, you
need to be sure that your Unleash Admin already have
created the custom project roles in your Unleash
instance so you can assign your project members from
the project access page.
</Typography>
}
/>
</Dialogue>
</PageContent>
);
<PremiumFeature
origin={PlausibleOrigin.CHANGE_REQUEST}
enterpriseOnly
center
>
<>
If you want to use{' '}
<StyledLink
href={'https://www.getunleash.io/plans'} // TODO: Add link to change request docs when available
target="_blank"
>
"Change Requests"
</StyledLink>{' '}
you will need to upgrade to Enterprise plan
</>
</PremiumFeature>
</PageContent>
);
}
if (!hasAccess(UPDATE_PROJECT, projectId)) {
return (
<PageContent header={<PageHeader title="Project access" />}>
<Alert severity="error">
You need project owner permissions to access this section.
</Alert>
</PageContent>
);
}
return <ChangeRequestTable />;
};

View File

@ -0,0 +1,313 @@
import React, { useContext, useMemo, useState, VFC } from 'react';
import { HeaderGroup, useGlobalFilter, useTable } from 'react-table';
import { Alert, Box, styled, Typography } from '@mui/material';
import {
SortableTableHeader,
Table,
TableBody,
TableCell,
TableRow,
} from 'component/common/Table';
import { sortTypes } from 'utils/sortTypes';
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import PermissionSwitch from 'component/common/PermissionSwitch/PermissionSwitch';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useChangeRequestConfig } from 'hooks/api/getters/useChangeRequestConfig/useChangeRequestConfig';
import {
IChangeRequestConfig,
useChangeRequestApi,
} from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
import { UPDATE_PROJECT } from '@server/types/permissions';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { ChangeRequestProcessHelp } from './ChangeRequestProcessHelp/ChangeRequestProcessHelp';
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import { KeyboardArrowDownOutlined } from '@mui/icons-material';
import { useTheme } from '@mui/material/styles';
import AccessContext from 'contexts/AccessContext';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
const StyledBox = styled(Box)(({ theme }) => ({
padding: theme.spacing(1),
display: 'flex',
justifyContent: 'center',
'& .MuiInputBase-input': {
fontSize: theme.fontSizes.smallBody,
},
}));
export const ChangeRequestTable: VFC = () => {
const { trackEvent } = usePlausibleTracker();
const [dialogState, setDialogState] = useState<{
isOpen: boolean;
enableEnvironment: string;
isEnabled: boolean;
requiredApprovals: number;
}>({
isOpen: false,
enableEnvironment: '',
isEnabled: false,
requiredApprovals: 1,
});
const theme = useTheme();
const projectId = useRequiredPathParam('projectId');
const { data, loading, refetchChangeRequestConfig } =
useChangeRequestConfig(projectId);
const { updateChangeRequestEnvironmentConfig } = useChangeRequestApi();
const { setToastData, setToastApiError } = useToast();
const onRowChange =
(
enableEnvironment: string,
isEnabled: boolean,
requiredApprovals: number
) =>
() => {
setDialogState({
isOpen: true,
enableEnvironment,
isEnabled,
requiredApprovals,
});
};
const onConfirm = async () => {
if (dialogState.enableEnvironment) {
await updateConfiguration();
}
setDialogState(state => ({ ...state, isOpen: false }));
};
async function updateConfiguration(config?: IChangeRequestConfig) {
try {
await updateChangeRequestEnvironmentConfig(
config || {
project: projectId,
environment: dialogState.enableEnvironment,
enabled: !dialogState.isEnabled,
requiredApprovals: dialogState.requiredApprovals,
}
);
setToastData({
type: 'success',
title: 'Updated change request status',
text: 'Successfully updated change request status.',
});
await refetchChangeRequestConfig();
} catch (error) {
setToastApiError(formatUnknownError(error));
}
}
const approvalOptions = Array.from(Array(10).keys())
.map(key => String(key + 1))
.map(key => {
const labelText = key === '1' ? 'approval' : 'approvals';
return {
key,
label: `${key} ${labelText}`,
sx: { 'font-size': theme.fontSizes.smallBody },
};
});
function onRequiredApprovalsChange(original: any, approvals: string) {
updateConfiguration({
project: projectId,
environment: original.environment,
enabled: original.changeRequestEnabled,
requiredApprovals: Number(approvals),
});
}
const columns = useMemo(
() => [
{
Header: 'Environment',
accessor: 'environment',
disableSortBy: true,
},
{
Header: 'Type',
accessor: 'type',
disableGlobalFilter: true,
disableSortBy: true,
},
{
Header: 'Required approvals',
align: 'center',
Cell: ({ row: { original } }: any) => {
const { hasAccess } = useContext(AccessContext);
return (
<ConditionallyRender
condition={original.changeRequestEnabled}
show={
<StyledBox data-loading>
<GeneralSelect
options={approvalOptions}
value={original.requiredApprovals || 1}
onChange={approvals => {
onRequiredApprovalsChange(
original,
approvals
);
}}
disabled={
!hasAccess(
UPDATE_PROJECT,
projectId
)
}
IconComponent={
KeyboardArrowDownOutlined
}
fullWidth
/>
</StyledBox>
}
/>
);
},
width: 100,
disableGlobalFilter: true,
disableSortBy: true,
},
{
Header: 'Status',
accessor: 'changeRequestEnabled',
id: 'changeRequestEnabled',
align: 'center',
Cell: ({ value, row: { original } }: any) => (
<StyledBox data-loading>
<PermissionSwitch
checked={value}
projectId={projectId}
permission={UPDATE_PROJECT}
inputProps={{ 'aria-label': original.environment }}
onClick={onRowChange(
original.environment,
original.changeRequestEnabled,
original.requiredApprovals
)}
/>
</StyledBox>
),
width: 100,
disableGlobalFilter: true,
disableSortBy: true,
},
],
[]
);
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
useTable(
{
// @ts-ignore
columns,
data,
sortTypes,
autoResetGlobalFilter: false,
disableSortRemove: true,
defaultColumn: {
Cell: TextCell,
},
},
useGlobalFilter
);
return (
<PageContent
header={
<PageHeader
titleElement="Change request configuration"
actions={<ChangeRequestProcessHelp />}
/>
}
isLoading={loading}
>
<Alert severity="info" sx={{ mb: 3 }}>
If change request is enabled for an environment, then any change
in that environment needs to be approved before it will be
applied
</Alert>
<Table {...getTableProps()}>
<SortableTableHeader
headerGroups={headerGroups as HeaderGroup<object>[]}
/>
<TableBody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row);
return (
<TableRow hover {...row.getRowProps()}>
{row.cells.map(cell => (
<TableCell {...cell.getCellProps()}>
{cell.render('Cell')}
</TableCell>
))}
</TableRow>
);
})}
</TableBody>
</Table>
<Dialogue
onClick={() => {
trackEvent('change_request', {
props: {
eventType: `change request ${
!dialogState.isEnabled ? 'enabled' : 'disabled'
}`,
},
});
onConfirm();
}}
open={dialogState.isOpen}
onClose={() =>
setDialogState(state => ({ ...state, isOpen: false }))
}
primaryButtonText={dialogState.isEnabled ? 'Disable' : 'Enable'}
secondaryButtonText="Cancel"
title={`${
dialogState.isEnabled ? 'Disable' : 'Enable'
} change requests`}
>
<Typography sx={{ mb: 1 }}>
You are about to{' '}
{dialogState.isEnabled ? 'disable' : 'enable'} Change
request
<ConditionallyRender
condition={Boolean(dialogState.enableEnvironment)}
show={
<>
{' '}
for{' '}
<strong>{dialogState.enableEnvironment}</strong>
</>
}
/>
.
</Typography>
<ConditionallyRender
condition={!dialogState.isEnabled}
show={
<Typography variant="body2" color="text.secondary">
When enabling change request for an environment, you
need to be sure that your Unleash Admin already have
created the custom project roles in your Unleash
instance so you can assign your project members from
the project access page.
</Typography>
}
/>
</Dialogue>
</PageContent>
);
};

View File

@ -1,9 +1,9 @@
import {
Navigate,
Route,
Routes,
useLocation,
useNavigate,
Navigate,
} from 'react-router-dom';
import { ITab, VerticalTabs } from 'component/common/VerticalTabs/VerticalTabs';
import { ProjectAccess } from 'component/project/ProjectAccess/ProjectAccess';
@ -14,10 +14,19 @@ export const ProjectSettings = () => {
const location = useLocation();
const navigate = useNavigate();
const tabs = [
{ id: 'access', label: 'Access' },
{ id: 'environments', label: 'Environments' },
{ id: 'change-requests', label: 'Change request configuration' },
const tabs: ITab[] = [
{
id: 'environments',
label: 'Environments',
},
{
id: 'access',
label: 'Access',
},
{
id: 'change-requests',
label: 'Change request configuration',
},
];
const onChange = (tab: ITab) => {
@ -35,13 +44,13 @@ export const ProjectSettings = () => {
onChange={onChange}
>
<Routes>
<Route path={`${tabs[0].id}/*`} element={<ProjectAccess />} />
<Route
path={`${tabs[1].id}/*`}
path="environments/*"
element={<ProjectEnvironmentList />}
/>
<Route path="access/*" element={<ProjectAccess />} />
<Route
path={`${tabs[2].id}/*`}
path="change-requests/*"
element={<ChangeRequestConfiguration />}
/>
<Route

View File

@ -1,7 +1,7 @@
import { useContext } from 'react';
import { PageContent } from 'component/common/PageContent/PageContent';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { Alert } from '@mui/material';
import { Alert, Box, Link, styled } from '@mui/material';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import AccessContext from 'contexts/AccessContext';
import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
@ -9,6 +9,15 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { usePageTitle } from 'hooks/usePageTitle';
import { ProjectAccessTable } from 'component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable';
import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject';
import {
PlausibleOrigin,
PremiumFeature,
} from 'component/common/PremiumFeature/PremiumFeature';
const StyledLink = styled(Link)(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
width: 'fit-content',
}));
export const ProjectAccess = () => {
const projectId = useRequiredPathParam('projectId');
@ -20,18 +29,29 @@ export const ProjectAccess = () => {
if (isOss()) {
return (
<PageContent header={<PageHeader title="Project access" />}>
<Alert severity="error">
Controlling access to projects requires a paid version of
Unleash. Check out{' '}
<a
href="https://www.getunleash.io"
target="_blank"
rel="noreferrer"
>
getunleash.io
</a>{' '}
to find out more.
</Alert>
<Box
sx={{
display: 'inline-flex',
maxWidth: '50%',
margin: '0 25%',
}}
alignSelf={'center'}
>
<PremiumFeature origin={PlausibleOrigin.ACCESS} center>
<>
Controlling access to projects requires a paid
version of Unleash. Check out{' '}
<StyledLink
href="https://www.getunleash.io"
target="_blank"
rel="noreferrer"
>
getunleash.io
</StyledLink>{' '}
to find out more.
</>
</PremiumFeature>
</Box>
</PageContent>
);
}

View File

@ -8,7 +8,6 @@ import { ProjectCard } from '../ProjectCard/ProjectCard';
import { useStyles } from './ProjectList.styles';
import { IProjectCard } from 'interfaces/project';
import loadingData from './loadingData';
import useLoading from 'hooks/useLoading';
import { PageContent } from 'component/common/PageContent/PageContent';
import AccessContext from 'contexts/AccessContext';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
@ -21,7 +20,10 @@ import { TablePlaceholder } from 'component/common/Table';
import { useMediaQuery } from '@mui/material';
import theme from 'themes/theme';
import { Search } from 'component/common/Search/Search';
import { ProFeatureTooltip } from 'component/common/ProFeatureTooltip/ProFeatureTooltip';
import {
PlausibleOrigin,
PremiumFeature,
} from 'component/common/PremiumFeature/PremiumFeature';
import { ITooltipResolverProps } from 'component/common/TooltipResolver/TooltipResolver';
import { ReactComponent as ProPlanIcon } from 'assets/icons/pro-enterprise-feature-badge.svg';
@ -46,10 +48,10 @@ function resolveCreateButtonData(
disabled: true,
tooltip: {
titleComponent: (
<ProFeatureTooltip>
<PremiumFeature origin={PlausibleOrigin.PROJECT}>
To be able to add more projects you need to upgrade to
Pro or Enterprise plan
</ProFeatureTooltip>
</PremiumFeature>
),
sx: { maxWidth: '320px' },
variant: 'custom',