diff --git a/frontend/src/assets/icons/milestone.svg b/frontend/src/assets/icons/milestone.svg new file mode 100644 index 000000000000..1b69140cc6ff --- /dev/null +++ b/frontend/src/assets/icons/milestone.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/component/releases/ReleasePlanTemplate/CreateReleasePlanTemplate.tsx b/frontend/src/component/releases/ReleasePlanTemplate/CreateReleasePlanTemplate.tsx index 77ab70e4d8b9..621bc1163017 100644 --- a/frontend/src/component/releases/ReleasePlanTemplate/CreateReleasePlanTemplate.tsx +++ b/frontend/src/component/releases/ReleasePlanTemplate/CreateReleasePlanTemplate.tsx @@ -11,6 +11,7 @@ import { scrollToTop } from 'component/common/util'; import useToast from 'hooks/useToast'; import { formatUnknownError } from 'utils/formatUnknownError'; import { useUiFlag } from 'hooks/useUiFlag'; +import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; const StyledButtonContainer = styled('div')(() => ({ marginTop: 'auto', @@ -23,6 +24,7 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({ })); export const CreateReleasePlanTemplate = () => { + const { uiConfig } = useUiConfig(); const releasePlansEnabled = useUiFlag('releasePlans'); const { setToastApiError, setToastData } = useToast(); const navigate = useNavigate(); @@ -50,12 +52,10 @@ export const CreateReleasePlanTemplate = () => { clearErrors(); const isValid = validate(); if (isValid) { - const payload = getTemplatePayload(); try { - const template = await createReleasePlanTemplate({ - ...payload, - milestones, - }); + const template = await createReleasePlanTemplate( + getTemplatePayload(), + ); scrollToTop(); setToastData({ 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) { return null; } @@ -83,7 +90,7 @@ export const CreateReleasePlanTemplate = () => { errors={errors} clearErrors={clearErrors} 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} > diff --git a/frontend/src/component/releases/ReleasePlanTemplate/EditReleasePlanTemplate.tsx b/frontend/src/component/releases/ReleasePlanTemplate/EditReleasePlanTemplate.tsx index 1a4796f9f36b..87113926d753 100644 --- a/frontend/src/component/releases/ReleasePlanTemplate/EditReleasePlanTemplate.tsx +++ b/frontend/src/component/releases/ReleasePlanTemplate/EditReleasePlanTemplate.tsx @@ -11,6 +11,7 @@ import { useNavigate } from 'react-router-dom'; import { formatUnknownError } from 'utils/formatUnknownError'; import useToast from 'hooks/useToast'; import useReleasePlanTemplatesApi from 'hooks/api/actions/useReleasePlanTemplatesApi/useReleasePlanTemplatesApi'; +import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; const StyledButtonContainer = styled('div')(() => ({ marginTop: 'auto', @@ -23,6 +24,7 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({ })); export const EditReleasePlanTemplate = () => { + const { uiConfig } = useUiConfig(); const releasePlansEnabled = useUiFlag('releasePlans'); const templateId = useRequiredPathParam('templateId'); const { template, loading, error, refetch } = @@ -56,13 +58,11 @@ export const EditReleasePlanTemplate = () => { clearErrors(); const isValid = validate(); if (isValid) { - const payload = getTemplatePayload(); try { - await updateReleasePlanTemplate({ - ...payload, - id: templateId, - milestones, - }); + await updateReleasePlanTemplate( + templateId, + getTemplatePayload(), + ); await refetch(); setToastData({ 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) { return null; } @@ -89,7 +96,7 @@ export const EditReleasePlanTemplate = () => { errors={errors} clearErrors={clearErrors} 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} loading={loading} > diff --git a/frontend/src/component/releases/ReleasePlanTemplate/TemplateForm.tsx b/frontend/src/component/releases/ReleasePlanTemplate/TemplateForm.tsx index 58bfd823f443..9ac870b9a025 100644 --- a/frontend/src/component/releases/ReleasePlanTemplate/TemplateForm.tsx +++ b/frontend/src/component/releases/ReleasePlanTemplate/TemplateForm.tsx @@ -6,10 +6,10 @@ import type { IReleasePlanMilestoneStrategy, } from 'interfaces/releasePlans'; import FormTemplate from 'component/common/FormTemplate/FormTemplate'; -import ReleaseTemplateIcon from '@mui/icons-material/DashboardOutlined'; import { SidebarModal } from 'component/common/SidebarModal/SidebarModal'; import { useState } from 'react'; import { ReleasePlanTemplateAddStrategyForm } from './ReleasePlanTemplateAddStrategyForm'; +import { TemplateFormDescription } from './TemplateFormDescription'; const StyledInputDescription = styled('p')(({ theme }) => ({ marginBottom: theme.spacing(1), @@ -38,7 +38,7 @@ interface ITemplateFormProps { errors: { [key: string]: string }; clearErrors: () => void; formTitle: string; - formDescription: string; + formatApiCode: () => string; handleSubmit: (e: React.FormEvent) => void; loading?: boolean; children?: React.ReactNode; @@ -54,7 +54,7 @@ export const TemplateForm: React.FC = ({ errors, clearErrors, formTitle, - formDescription, + formatApiCode, handleSubmit, children, }) => { @@ -115,8 +115,8 @@ export const TemplateForm: React.FC = ({ return ( } + description={} + formatApiCode={formatApiCode} > diff --git a/frontend/src/component/releases/ReleasePlanTemplate/TemplateFormDescription.tsx b/frontend/src/component/releases/ReleasePlanTemplate/TemplateFormDescription.tsx new file mode 100644 index 000000000000..5d288cebb7d3 --- /dev/null +++ b/frontend/src/component/releases/ReleasePlanTemplate/TemplateFormDescription.tsx @@ -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 ( + + + + Release templates + +

+ 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. +

+

+ Customize templates to suit your needs by adding strategies to + specific milestones. Each milestone will execute sequentially, + streamlining your release process. +

+ + Example usage + +
+ + + Milestone 1 + +

+ Enable the feature for internal teams to test + functionality and resolve initial issues. +

+
+
+ + + Milestone 2 + +

+ Expand the rollout to 20% of beta users to gather + feedback and monitor performance. +

+
+
+ + + Milestone 3 + +

+ Release the feature to all users after confirming + stability and addressing earlier feedback. +

+
+
+
+
+ ); +}; diff --git a/frontend/src/component/releases/hooks/useTemplateForm.ts b/frontend/src/component/releases/hooks/useTemplateForm.ts index 28d070d6bd14..142faa1bd792 100644 --- a/frontend/src/component/releases/hooks/useTemplateForm.ts +++ b/frontend/src/component/releases/hooks/useTemplateForm.ts @@ -42,6 +42,7 @@ export const useTemplateForm = ( return { name, description, + milestones, }; }; diff --git a/frontend/src/hooks/api/actions/useReleasePlanTemplatesApi/useReleasePlanTemplatesApi.ts b/frontend/src/hooks/api/actions/useReleasePlanTemplatesApi/useReleasePlanTemplatesApi.ts index 0bef65e2ffe9..7d8509126819 100644 --- a/frontend/src/hooks/api/actions/useReleasePlanTemplatesApi/useReleasePlanTemplatesApi.ts +++ b/frontend/src/hooks/api/actions/useReleasePlanTemplatesApi/useReleasePlanTemplatesApi.ts @@ -1,4 +1,7 @@ -import type { IReleasePlanTemplatePayload } from 'interfaces/releasePlans'; +import type { + IReleasePlanTemplate, + IReleasePlanTemplatePayload, +} from 'interfaces/releasePlans'; import useAPI from '../useApi/useApi'; export const useReleasePlanTemplatesApi = () => { @@ -23,7 +26,7 @@ export const useReleasePlanTemplatesApi = () => { const createReleasePlanTemplate = async ( template: IReleasePlanTemplatePayload, - ): Promise => { + ): Promise => { const requestId = 'createReleasePlanTemplate'; const path = 'api/admin/release-plan-templates'; const req = createRequest( @@ -40,10 +43,11 @@ export const useReleasePlanTemplatesApi = () => { }; const updateReleasePlanTemplate = async ( + templateId: string, template: IReleasePlanTemplatePayload, ) => { const requestId = 'updateReleasePlanTemplate'; - const path = `api/admin/release-plan-templates/${template.id}`; + const path = `api/admin/release-plan-templates/${templateId}`; const req = createRequest( path, { diff --git a/frontend/src/interfaces/releasePlans.ts b/frontend/src/interfaces/releasePlans.ts index 4c50d5250322..ccce5fae8f01 100644 --- a/frontend/src/interfaces/releasePlans.ts +++ b/frontend/src/interfaces/releasePlans.ts @@ -41,10 +41,9 @@ export interface IReleasePlanMilestoneStrategy extends IFeatureStrategy { } export interface IReleasePlanTemplatePayload { - id?: string; name: string; description: string; - milestones?: IReleasePlanMilestonePayload[]; + milestones: IReleasePlanMilestonePayload[]; } export interface IReleasePlanMilestonePayload {