mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-28 17:55:15 +02:00
chore: release template sidebar (#8871)
https://linear.app/unleash/issue/2-3026/release-template-sidebar-documentationcommand Implements the release plan template form sidebar / description. Took some liberties in the text compared to what we had in our sketches. Also includes some slight refactoring. 
This commit is contained in:
parent
9044d4c537
commit
679e9d12ef
3
frontend/src/assets/icons/milestone.svg
Normal file
3
frontend/src/assets/icons/milestone.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4.48149 0.771598C5.27968 -0.159625 6.72032 -0.159624 7.51851 0.771599L10.8844 4.69842C11.5263 5.4474 11.5263 6.5526 10.8844 7.30158L7.51851 11.2284C6.72032 12.1596 5.27968 12.1596 4.48149 11.2284L1.11564 7.30158C0.47366 6.5526 0.47366 5.4474 1.11564 4.69842L4.48149 0.771598Z" fill="#FFC46F"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 407 B |
@ -11,6 +11,7 @@ import { scrollToTop } from 'component/common/util';
|
|||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
const StyledButtonContainer = styled('div')(() => ({
|
const StyledButtonContainer = styled('div')(() => ({
|
||||||
marginTop: 'auto',
|
marginTop: 'auto',
|
||||||
@ -23,6 +24,7 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export const CreateReleasePlanTemplate = () => {
|
export const CreateReleasePlanTemplate = () => {
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
const releasePlansEnabled = useUiFlag('releasePlans');
|
const releasePlansEnabled = useUiFlag('releasePlans');
|
||||||
const { setToastApiError, setToastData } = useToast();
|
const { setToastApiError, setToastData } = useToast();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -50,12 +52,10 @@ export const CreateReleasePlanTemplate = () => {
|
|||||||
clearErrors();
|
clearErrors();
|
||||||
const isValid = validate();
|
const isValid = validate();
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
const payload = getTemplatePayload();
|
|
||||||
try {
|
try {
|
||||||
const template = await createReleasePlanTemplate({
|
const template = await createReleasePlanTemplate(
|
||||||
...payload,
|
getTemplatePayload(),
|
||||||
milestones,
|
);
|
||||||
});
|
|
||||||
scrollToTop();
|
scrollToTop();
|
||||||
setToastData({
|
setToastData({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@ -68,6 +68,13 @@ export const CreateReleasePlanTemplate = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatApiCode = () => `curl --location --request POST '${
|
||||||
|
uiConfig.unleashUrl
|
||||||
|
}/api/admin/release-plan-templates' \\
|
||||||
|
--header 'Authorization: INSERT_API_KEY' \\
|
||||||
|
--header 'Content-Type: application/json' \\
|
||||||
|
--data-raw '${JSON.stringify(getTemplatePayload(), undefined, 2)}'`;
|
||||||
|
|
||||||
if (!releasePlansEnabled) {
|
if (!releasePlansEnabled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -83,7 +90,7 @@ export const CreateReleasePlanTemplate = () => {
|
|||||||
errors={errors}
|
errors={errors}
|
||||||
clearErrors={clearErrors}
|
clearErrors={clearErrors}
|
||||||
formTitle='Create release plan template'
|
formTitle='Create release plan template'
|
||||||
formDescription='Create a release plan template to make it easier for you and your team to release features.'
|
formatApiCode={formatApiCode}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
<StyledButtonContainer>
|
<StyledButtonContainer>
|
||||||
|
@ -11,6 +11,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import useReleasePlanTemplatesApi from 'hooks/api/actions/useReleasePlanTemplatesApi/useReleasePlanTemplatesApi';
|
import useReleasePlanTemplatesApi from 'hooks/api/actions/useReleasePlanTemplatesApi/useReleasePlanTemplatesApi';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
const StyledButtonContainer = styled('div')(() => ({
|
const StyledButtonContainer = styled('div')(() => ({
|
||||||
marginTop: 'auto',
|
marginTop: 'auto',
|
||||||
@ -23,6 +24,7 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export const EditReleasePlanTemplate = () => {
|
export const EditReleasePlanTemplate = () => {
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
const releasePlansEnabled = useUiFlag('releasePlans');
|
const releasePlansEnabled = useUiFlag('releasePlans');
|
||||||
const templateId = useRequiredPathParam('templateId');
|
const templateId = useRequiredPathParam('templateId');
|
||||||
const { template, loading, error, refetch } =
|
const { template, loading, error, refetch } =
|
||||||
@ -56,13 +58,11 @@ export const EditReleasePlanTemplate = () => {
|
|||||||
clearErrors();
|
clearErrors();
|
||||||
const isValid = validate();
|
const isValid = validate();
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
const payload = getTemplatePayload();
|
|
||||||
try {
|
try {
|
||||||
await updateReleasePlanTemplate({
|
await updateReleasePlanTemplate(
|
||||||
...payload,
|
templateId,
|
||||||
id: templateId,
|
getTemplatePayload(),
|
||||||
milestones,
|
);
|
||||||
});
|
|
||||||
await refetch();
|
await refetch();
|
||||||
setToastData({
|
setToastData({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@ -74,6 +74,13 @@ export const EditReleasePlanTemplate = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatApiCode = () => `curl --location --request PUT '${
|
||||||
|
uiConfig.unleashUrl
|
||||||
|
}/api/admin/release-plan-templates/${templateId}' \\
|
||||||
|
--header 'Authorization: INSERT_API_KEY' \\
|
||||||
|
--header 'Content-Type: application/json' \\
|
||||||
|
--data-raw '${JSON.stringify(getTemplatePayload(), undefined, 2)}'`;
|
||||||
|
|
||||||
if (!releasePlansEnabled) {
|
if (!releasePlansEnabled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -89,7 +96,7 @@ export const EditReleasePlanTemplate = () => {
|
|||||||
errors={errors}
|
errors={errors}
|
||||||
clearErrors={clearErrors}
|
clearErrors={clearErrors}
|
||||||
formTitle={`Edit template ${template.name}`}
|
formTitle={`Edit template ${template.name}`}
|
||||||
formDescription='Edit a release plan template that makes it easier for you and your team to release features.'
|
formatApiCode={formatApiCode}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
>
|
>
|
||||||
|
@ -6,10 +6,10 @@ import type {
|
|||||||
IReleasePlanMilestoneStrategy,
|
IReleasePlanMilestoneStrategy,
|
||||||
} from 'interfaces/releasePlans';
|
} from 'interfaces/releasePlans';
|
||||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
||||||
import ReleaseTemplateIcon from '@mui/icons-material/DashboardOutlined';
|
|
||||||
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ReleasePlanTemplateAddStrategyForm } from './ReleasePlanTemplateAddStrategyForm';
|
import { ReleasePlanTemplateAddStrategyForm } from './ReleasePlanTemplateAddStrategyForm';
|
||||||
|
import { TemplateFormDescription } from './TemplateFormDescription';
|
||||||
|
|
||||||
const StyledInputDescription = styled('p')(({ theme }) => ({
|
const StyledInputDescription = styled('p')(({ theme }) => ({
|
||||||
marginBottom: theme.spacing(1),
|
marginBottom: theme.spacing(1),
|
||||||
@ -38,7 +38,7 @@ interface ITemplateFormProps {
|
|||||||
errors: { [key: string]: string };
|
errors: { [key: string]: string };
|
||||||
clearErrors: () => void;
|
clearErrors: () => void;
|
||||||
formTitle: string;
|
formTitle: string;
|
||||||
formDescription: string;
|
formatApiCode: () => string;
|
||||||
handleSubmit: (e: React.FormEvent) => void;
|
handleSubmit: (e: React.FormEvent) => void;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@ -54,7 +54,7 @@ export const TemplateForm: React.FC<ITemplateFormProps> = ({
|
|||||||
errors,
|
errors,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
formTitle,
|
formTitle,
|
||||||
formDescription,
|
formatApiCode,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
@ -115,8 +115,8 @@ export const TemplateForm: React.FC<ITemplateFormProps> = ({
|
|||||||
return (
|
return (
|
||||||
<FormTemplate
|
<FormTemplate
|
||||||
title={formTitle}
|
title={formTitle}
|
||||||
description={formDescription}
|
description={<TemplateFormDescription />}
|
||||||
documentationIcon={<ReleaseTemplateIcon />}
|
formatApiCode={formatApiCode}
|
||||||
>
|
>
|
||||||
<StyledForm onSubmit={handleSubmit}>
|
<StyledForm onSubmit={handleSubmit}>
|
||||||
<StyledInputDescription>
|
<StyledInputDescription>
|
||||||
|
@ -0,0 +1,98 @@
|
|||||||
|
import ReleaseTemplateIcon from '@mui/icons-material/DashboardOutlined';
|
||||||
|
import { ReactComponent as MilestoneIcon } from 'assets/icons/milestone.svg';
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
|
||||||
|
const StyledDescription = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledDescriptionHeader = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
fontSize: theme.fontSizes.bodySize,
|
||||||
|
fontWeight: theme.fontWeight.bold,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledExampleUsage = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledMilestones = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledLabel = styled('p')(({ theme }) => ({
|
||||||
|
fontWeight: theme.fontWeight.bold,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledMilestoneHeader = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const TemplateFormDescription = () => {
|
||||||
|
return (
|
||||||
|
<StyledDescription>
|
||||||
|
<StyledDescriptionHeader>
|
||||||
|
<ReleaseTemplateIcon />
|
||||||
|
Release templates
|
||||||
|
</StyledDescriptionHeader>
|
||||||
|
<p>
|
||||||
|
Standardize your team's approach to rolling out new
|
||||||
|
functionality with release templates. These templates allow you
|
||||||
|
to predefine strategies, or groups of strategies, making it
|
||||||
|
easier to set up new flags and ensure alignment in how rollouts
|
||||||
|
are managed.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Customize templates to suit your needs by adding strategies to
|
||||||
|
specific milestones. Each milestone will execute sequentially,
|
||||||
|
streamlining your release process.
|
||||||
|
</p>
|
||||||
|
<StyledExampleUsage>
|
||||||
|
<StyledLabel>Example usage</StyledLabel>
|
||||||
|
<StyledMilestones>
|
||||||
|
<div>
|
||||||
|
<StyledMilestoneHeader>
|
||||||
|
<MilestoneIcon />
|
||||||
|
Milestone 1
|
||||||
|
</StyledMilestoneHeader>
|
||||||
|
<p>
|
||||||
|
Enable the feature for internal teams to test
|
||||||
|
functionality and resolve initial issues.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<StyledMilestoneHeader>
|
||||||
|
<MilestoneIcon />
|
||||||
|
Milestone 2
|
||||||
|
</StyledMilestoneHeader>
|
||||||
|
<p>
|
||||||
|
Expand the rollout to 20% of beta users to gather
|
||||||
|
feedback and monitor performance.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<StyledMilestoneHeader>
|
||||||
|
<MilestoneIcon />
|
||||||
|
Milestone 3
|
||||||
|
</StyledMilestoneHeader>
|
||||||
|
<p>
|
||||||
|
Release the feature to all users after confirming
|
||||||
|
stability and addressing earlier feedback.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</StyledMilestones>
|
||||||
|
</StyledExampleUsage>
|
||||||
|
</StyledDescription>
|
||||||
|
);
|
||||||
|
};
|
@ -42,6 +42,7 @@ export const useTemplateForm = (
|
|||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
|
milestones,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import type { IReleasePlanTemplatePayload } from 'interfaces/releasePlans';
|
import type {
|
||||||
|
IReleasePlanTemplate,
|
||||||
|
IReleasePlanTemplatePayload,
|
||||||
|
} from 'interfaces/releasePlans';
|
||||||
import useAPI from '../useApi/useApi';
|
import useAPI from '../useApi/useApi';
|
||||||
|
|
||||||
export const useReleasePlanTemplatesApi = () => {
|
export const useReleasePlanTemplatesApi = () => {
|
||||||
@ -23,7 +26,7 @@ export const useReleasePlanTemplatesApi = () => {
|
|||||||
|
|
||||||
const createReleasePlanTemplate = async (
|
const createReleasePlanTemplate = async (
|
||||||
template: IReleasePlanTemplatePayload,
|
template: IReleasePlanTemplatePayload,
|
||||||
): Promise<IReleasePlanTemplatePayload> => {
|
): Promise<IReleasePlanTemplate> => {
|
||||||
const requestId = 'createReleasePlanTemplate';
|
const requestId = 'createReleasePlanTemplate';
|
||||||
const path = 'api/admin/release-plan-templates';
|
const path = 'api/admin/release-plan-templates';
|
||||||
const req = createRequest(
|
const req = createRequest(
|
||||||
@ -40,10 +43,11 @@ export const useReleasePlanTemplatesApi = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateReleasePlanTemplate = async (
|
const updateReleasePlanTemplate = async (
|
||||||
|
templateId: string,
|
||||||
template: IReleasePlanTemplatePayload,
|
template: IReleasePlanTemplatePayload,
|
||||||
) => {
|
) => {
|
||||||
const requestId = 'updateReleasePlanTemplate';
|
const requestId = 'updateReleasePlanTemplate';
|
||||||
const path = `api/admin/release-plan-templates/${template.id}`;
|
const path = `api/admin/release-plan-templates/${templateId}`;
|
||||||
const req = createRequest(
|
const req = createRequest(
|
||||||
path,
|
path,
|
||||||
{
|
{
|
||||||
|
@ -41,10 +41,9 @@ export interface IReleasePlanMilestoneStrategy extends IFeatureStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IReleasePlanTemplatePayload {
|
export interface IReleasePlanTemplatePayload {
|
||||||
id?: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
milestones?: IReleasePlanMilestonePayload[];
|
milestones: IReleasePlanMilestonePayload[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReleasePlanMilestonePayload {
|
export interface IReleasePlanMilestonePayload {
|
||||||
|
Loading…
Reference in New Issue
Block a user