1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-12-09 20:04:11 +01:00

chore: add warning about release plans in import-export (#10805)

https://linear.app/unleash/issue/2-3965/add-a-note-if-were-exporting-that-we-dont-understand-release-plans-in

Adds a warning about release plans in import/export.

It's not trivial to know every flag that will be exported in every
scenario, and whether they have release plans, so our logic here is
"have you configured release templates?"

<img width="706" height="516" alt="image"
src="https://github.com/user-attachments/assets/68ba8618-9887-491c-b46e-256b45700d74"
/>

<img width="732" height="503" alt="image"
src="https://github.com/user-attachments/assets/086e37d4-78ae-4647-93a2-5d1845c2758a"
/>
This commit is contained in:
Nuno Góis 2025-10-15 14:44:30 +01:00 committed by GitHub
parent f3ab70aeca
commit 4ff41fa6a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 65 additions and 36 deletions

View File

@ -1,5 +1,5 @@
import { createRef, useState } from 'react'; import { createRef, useState } from 'react';
import { styled, Typography, Box } from '@mui/material'; import { styled, Typography, Box, Alert } from '@mui/material';
import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { Dialogue } from 'component/common/Dialogue/Dialogue';
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect'; import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import { useExportApi } from 'hooks/api/actions/useExportApi/useExportApi'; import { useExportApi } from 'hooks/api/actions/useExportApi/useExportApi';
@ -8,10 +8,11 @@ import type { FeatureSchema } from 'openapi';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { ConditionallyRender } from '../../common/ConditionallyRender/ConditionallyRender.tsx'; import { ConditionallyRender } from '../../common/ConditionallyRender/ConditionallyRender.tsx';
import { useReleasePlanTemplates } from 'hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplates.ts';
interface IExportDialogProps { interface IExportDialogProps {
showExportDialog: boolean; showExportDialog: boolean;
data: Pick<FeatureSchema, 'name'>[]; data: Pick<FeatureSchema, 'name' | 'environments'>[];
project?: string; project?: string;
onClose: () => void; onClose: () => void;
onConfirm?: () => void; onConfirm?: () => void;
@ -35,6 +36,8 @@ export const ExportDialog = ({
const { createExport } = useExportApi(); const { createExport } = useExportApi();
const ref = createRef<HTMLDivElement>(); const ref = createRef<HTMLDivElement>();
const { setToastApiError } = useToast(); const { setToastApiError } = useToast();
const { templates } = useReleasePlanTemplates();
const hasReleaseTemplates = Boolean(templates.length);
const getOptions = () => const getOptions = () =>
environments.map((env) => ({ environments.map((env) => ({
@ -88,6 +91,13 @@ export const ExportDialog = ({
secondaryButtonText='Cancel' secondaryButtonText='Cancel'
> >
<Box ref={ref}> <Box ref={ref}>
{hasReleaseTemplates && (
<Alert severity='warning' sx={{ mb: 4 }}>
Exporting does not include release plans. You may need
to set up new release plans for the imported feature
flags.
</Alert>
)}
<ConditionallyRender <ConditionallyRender
condition={data.length > 0} condition={data.length > 0}
show={ show={

View File

@ -1,5 +1,6 @@
import type { FC } from 'react'; import type { FC } from 'react';
import { Box, styled, Typography } from '@mui/material'; import { Box, styled, Typography } from '@mui/material';
import { useReleasePlanTemplates } from 'hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplates';
const ImportExplanationContainer = styled(Box)(({ theme }) => ({ const ImportExplanationContainer = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.background.elevation2, backgroundColor: theme.palette.background.elevation2,
@ -17,7 +18,10 @@ const ImportExplanationDescription = styled(Box)(({ theme }) => ({
fontSize: theme.fontSizes.smallBody, fontSize: theme.fontSizes.smallBody,
})); }));
export const ImportExplanation: FC = () => ( export const ImportExplanation: FC = () => {
const { templates } = useReleasePlanTemplates();
const hasReleaseTemplates = Boolean(templates.length);
return (
<ImportExplanationContainer> <ImportExplanationContainer>
<ImportExplanationHeader> <ImportExplanationHeader>
What is being imported? What is being imported?
@ -36,14 +40,23 @@ export const ImportExplanation: FC = () => (
</ImportExplanationDescription> </ImportExplanationDescription>
<ImportExplanationHeader>Exceptions?</ImportExplanationHeader> <ImportExplanationHeader>Exceptions?</ImportExplanationHeader>
<ImportExplanationDescription> <ImportExplanationDescription>
If the feature flag already exists in the new instance, it will be If the feature flag already exists in the new instance, it will
overwritten be overwritten
</ImportExplanationDescription> </ImportExplanationDescription>
<ImportExplanationHeader>What is not imported?</ImportExplanationHeader> <ImportExplanationHeader>
What is not imported?
</ImportExplanationHeader>
<ImportExplanationDescription sx={{ marginBottom: 0 }}> <ImportExplanationDescription sx={{ marginBottom: 0 }}>
If we detect segments or custom strategies in your imported file, we If we detect segments or custom strategies in your imported
will stop the import. You need to create them first in the new file, we will stop the import. You need to create them first in
instance and run the import again the new instance and run the import again
</ImportExplanationDescription> </ImportExplanationDescription>
{hasReleaseTemplates && (
<ImportExplanationDescription sx={{ marginTop: 2 }}>
Release plans are not included in the import. You may need
to set up new release plans for the imported feature flags
</ImportExplanationDescription>
)}
</ImportExplanationContainer> </ImportExplanationContainer>
); );
};

View File

@ -1,5 +1,5 @@
import { type FC, type ReactNode, useState } from 'react'; import { type FC, type ReactNode, useState } from 'react';
import { Box, IconButton, Tooltip } from '@mui/material'; import { Box, IconButton, styled, Tooltip } from '@mui/material';
import useLoading from 'hooks/useLoading'; import useLoading from 'hooks/useLoading';
import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
@ -8,6 +8,10 @@ import IosShare from '@mui/icons-material/IosShare';
import { FlagCreationButton } from './FlagCreationButton/FlagCreationButton.tsx'; import { FlagCreationButton } from './FlagCreationButton/FlagCreationButton.tsx';
import { ImportButton } from './ImportButton/ImportButton.tsx'; import { ImportButton } from './ImportButton/ImportButton.tsx';
const StyledIconButton = styled(IconButton)(({ theme }) => ({
padding: theme.spacing(1.5),
}));
type ProjectFeatureTogglesHeaderProps = { type ProjectFeatureTogglesHeaderProps = {
isLoading?: boolean; isLoading?: boolean;
totalItems?: number; totalItems?: number;
@ -39,12 +43,12 @@ export const ProjectFeatureTogglesHeader: FC<
<> <>
{actions} {actions}
<Tooltip title='Export all project flags' arrow> <Tooltip title='Export all project flags' arrow>
<IconButton <StyledIconButton
data-loading data-loading
onClick={() => setShowExportDialog(true)} onClick={() => setShowExportDialog(true)}
> >
<IosShare /> <IosShare />
</IconButton> </StyledIconButton>
</Tooltip> </Tooltip>
<ImportButton /> <ImportButton />

View File

@ -335,7 +335,8 @@ export default class FakeFeatureStrategiesStore
this.featureStrategies.filter( this.featureStrategies.filter(
(strategy) => (strategy) =>
features.includes(strategy.featureName) && features.includes(strategy.featureName) &&
strategy.environment === environment, strategy.environment === environment &&
!strategy.milestoneId,
), ),
); );
} }

View File

@ -278,6 +278,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
.select(COLUMNS) .select(COLUMNS)
.from<IFeatureStrategiesTable>(T.featureStrategies) .from<IFeatureStrategiesTable>(T.featureStrategies)
.whereIn('feature_name', features) .whereIn('feature_name', features)
.andWhere('milestone_id', null)
.orderBy('feature_name', 'asc'); .orderBy('feature_name', 'asc');
if (environment) { if (environment) {
query.where('environment', environment); query.where('environment', environment);