mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: export all features in project (#5677)
This commit is contained in:
		
							parent
							
								
									1043efd89f
								
							
						
					
					
						commit
						7800d9d1b4
					
				@ -7,10 +7,12 @@ import useToast from 'hooks/useToast';
 | 
				
			|||||||
import type { FeatureSchema } from 'openapi';
 | 
					import type { FeatureSchema } from 'openapi';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { formatUnknownError } from 'utils/formatUnknownError';
 | 
					import { formatUnknownError } from 'utils/formatUnknownError';
 | 
				
			||||||
 | 
					import { ConditionallyRender } from '../../common/ConditionallyRender/ConditionallyRender';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface IExportDialogProps {
 | 
					interface IExportDialogProps {
 | 
				
			||||||
    showExportDialog: boolean;
 | 
					    showExportDialog: boolean;
 | 
				
			||||||
    data: Pick<FeatureSchema, 'name'>[];
 | 
					    data: Pick<FeatureSchema, 'name'>[];
 | 
				
			||||||
 | 
					    project?: string;
 | 
				
			||||||
    onClose: () => void;
 | 
					    onClose: () => void;
 | 
				
			||||||
    onConfirm?: () => void;
 | 
					    onConfirm?: () => void;
 | 
				
			||||||
    environments: string[];
 | 
					    environments: string[];
 | 
				
			||||||
@ -24,6 +26,7 @@ const StyledSelect = styled(GeneralSelect)(({ theme }) => ({
 | 
				
			|||||||
export const ExportDialog = ({
 | 
					export const ExportDialog = ({
 | 
				
			||||||
    showExportDialog,
 | 
					    showExportDialog,
 | 
				
			||||||
    data,
 | 
					    data,
 | 
				
			||||||
 | 
					    project,
 | 
				
			||||||
    onClose,
 | 
					    onClose,
 | 
				
			||||||
    onConfirm,
 | 
					    onConfirm,
 | 
				
			||||||
    environments,
 | 
					    environments,
 | 
				
			||||||
@ -63,6 +66,7 @@ export const ExportDialog = ({
 | 
				
			|||||||
            const payload = {
 | 
					            const payload = {
 | 
				
			||||||
                features: data.map((feature) => feature.name),
 | 
					                features: data.map((feature) => feature.name),
 | 
				
			||||||
                environment: selected,
 | 
					                environment: selected,
 | 
				
			||||||
 | 
					                project,
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
            const res = await createExport(payload);
 | 
					            const res = await createExport(payload);
 | 
				
			||||||
            const body = await res.json();
 | 
					            const body = await res.json();
 | 
				
			||||||
@ -84,9 +88,23 @@ export const ExportDialog = ({
 | 
				
			|||||||
            secondaryButtonText='Cancel'
 | 
					            secondaryButtonText='Cancel'
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
            <Box ref={ref}>
 | 
					            <Box ref={ref}>
 | 
				
			||||||
                The current search filter will be used to export feature
 | 
					                <ConditionallyRender
 | 
				
			||||||
                toggles. Currently {data.length} feature toggles will be
 | 
					                    condition={data.length > 0}
 | 
				
			||||||
                exported.
 | 
					                    show={
 | 
				
			||||||
 | 
					                        <span>
 | 
				
			||||||
 | 
					                            The current search filter will be used to export
 | 
				
			||||||
 | 
					                            feature toggles. Currently {data.length} feature
 | 
				
			||||||
 | 
					                            toggles will be exported.
 | 
				
			||||||
 | 
					                        </span>
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    elseShow={
 | 
				
			||||||
 | 
					                        <span>
 | 
				
			||||||
 | 
					                            You will export all feature toggles from this
 | 
				
			||||||
 | 
					                            project.
 | 
				
			||||||
 | 
					                        </span>
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <br />
 | 
					                <br />
 | 
				
			||||||
                <br />
 | 
					                <br />
 | 
				
			||||||
                <Typography>
 | 
					                <Typography>
 | 
				
			||||||
 | 
				
			|||||||
@ -42,7 +42,6 @@ export const ProjectFeatureTogglesHeader: VFC<
 | 
				
			|||||||
    totalItems,
 | 
					    totalItems,
 | 
				
			||||||
    searchQuery,
 | 
					    searchQuery,
 | 
				
			||||||
    onChangeSearchQuery,
 | 
					    onChangeSearchQuery,
 | 
				
			||||||
    dataToExport,
 | 
					 | 
				
			||||||
    environmentsToExport,
 | 
					    environmentsToExport,
 | 
				
			||||||
    actions,
 | 
					    actions,
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
@ -100,7 +99,7 @@ export const ProjectFeatureTogglesHeader: VFC<
 | 
				
			|||||||
                            show={
 | 
					                            show={
 | 
				
			||||||
                                <>
 | 
					                                <>
 | 
				
			||||||
                                    <Tooltip
 | 
					                                    <Tooltip
 | 
				
			||||||
                                        title='Export toggles visible in the table below'
 | 
					                                        title='Export all project toggles'
 | 
				
			||||||
                                        arrow
 | 
					                                        arrow
 | 
				
			||||||
                                    >
 | 
					                                    >
 | 
				
			||||||
                                        <IconButton
 | 
					                                        <IconButton
 | 
				
			||||||
@ -123,7 +122,8 @@ export const ProjectFeatureTogglesHeader: VFC<
 | 
				
			|||||||
                                                showExportDialog={
 | 
					                                                showExportDialog={
 | 
				
			||||||
                                                    showExportDialog
 | 
					                                                    showExportDialog
 | 
				
			||||||
                                                }
 | 
					                                                }
 | 
				
			||||||
                                                data={dataToExport || []}
 | 
					                                                project={projectId}
 | 
				
			||||||
 | 
					                                                data={[]}
 | 
				
			||||||
                                                onClose={() =>
 | 
					                                                onClose={() =>
 | 
				
			||||||
                                                    setShowExportDialog(false)
 | 
					                                                    setShowExportDialog(false)
 | 
				
			||||||
                                                }
 | 
					                                                }
 | 
				
			||||||
 | 
				
			|||||||
@ -786,10 +786,21 @@ export default class ExportImportService
 | 
				
			|||||||
        userName: string,
 | 
					        userName: string,
 | 
				
			||||||
        userId: number,
 | 
					        userId: number,
 | 
				
			||||||
    ): Promise<ExportResultSchema> {
 | 
					    ): Promise<ExportResultSchema> {
 | 
				
			||||||
        const featureNames =
 | 
					        let featureNames: string[] = [];
 | 
				
			||||||
            typeof query.tag === 'string'
 | 
					        if (typeof query.tag === 'string') {
 | 
				
			||||||
                ? await this.featureTagService.listFeatures(query.tag)
 | 
					            featureNames = await this.featureTagService.listFeatures(query.tag);
 | 
				
			||||||
                : (query.features as string[]) || [];
 | 
					        } else if (Array.isArray(query.features) && query.features.length) {
 | 
				
			||||||
 | 
					            featureNames = query.features;
 | 
				
			||||||
 | 
					        } else if (typeof query.project === 'string') {
 | 
				
			||||||
 | 
					            const allProjectFeatures = await this.toggleStore.getAll({
 | 
				
			||||||
 | 
					                project: query.project,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            featureNames = allProjectFeatures.map((feature) => feature.name);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            const allFeatures = await this.toggleStore.getAll();
 | 
				
			||||||
 | 
					            featureNames = allFeatures.map((feature) => feature.name);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const [
 | 
					        const [
 | 
				
			||||||
            features,
 | 
					            features,
 | 
				
			||||||
            featureEnvironments,
 | 
					            featureEnvironments,
 | 
				
			||||||
 | 
				
			|||||||
@ -548,7 +548,7 @@ test('should export tags', async () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('returns no features, when no feature was requested', async () => {
 | 
					test('returns all features, when no explicit feature was requested', async () => {
 | 
				
			||||||
    await createProjects();
 | 
					    await createProjects();
 | 
				
			||||||
    await createToggle({
 | 
					    await createToggle({
 | 
				
			||||||
        name: defaultFeatureName,
 | 
					        name: defaultFeatureName,
 | 
				
			||||||
@ -567,7 +567,41 @@ test('returns no features, when no feature was requested', async () => {
 | 
				
			|||||||
        .set('Content-Type', 'application/json')
 | 
					        .set('Content-Type', 'application/json')
 | 
				
			||||||
        .expect(200);
 | 
					        .expect(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    expect(body.features).toHaveLength(0);
 | 
					    expect(body.features).toHaveLength(2);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('returns all project features', async () => {
 | 
				
			||||||
 | 
					    await createProjects();
 | 
				
			||||||
 | 
					    await createToggle({
 | 
				
			||||||
 | 
					        name: defaultFeatureName,
 | 
				
			||||||
 | 
					        description: 'the #1 feature',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    await createToggle({
 | 
				
			||||||
 | 
					        name: 'second_feature',
 | 
				
			||||||
 | 
					        description: 'the #1 feature',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    const { body } = await app.request
 | 
				
			||||||
 | 
					        .post('/api/admin/features-batch/export')
 | 
				
			||||||
 | 
					        .send({
 | 
				
			||||||
 | 
					            environment: 'default',
 | 
				
			||||||
 | 
					            project: DEFAULT_PROJECT,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .set('Content-Type', 'application/json')
 | 
				
			||||||
 | 
					        .expect(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(body.features).toHaveLength(2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { body: otherProject } = await app.request
 | 
				
			||||||
 | 
					        .post('/api/admin/features-batch/export')
 | 
				
			||||||
 | 
					        .send({
 | 
				
			||||||
 | 
					            environment: 'default',
 | 
				
			||||||
 | 
					            features: [], // should be ignored because we have project
 | 
				
			||||||
 | 
					            project: 'other_project',
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .set('Content-Type', 'application/json')
 | 
				
			||||||
 | 
					        .expect(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(otherProject.features).toHaveLength(0);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const variants: VariantsSchema = [
 | 
					const variants: VariantsSchema = [
 | 
				
			||||||
 | 
				
			|||||||
@ -305,6 +305,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
 | 
				
			|||||||
    async getAllByNames(names: string[]): Promise<FeatureToggle[]> {
 | 
					    async getAllByNames(names: string[]): Promise<FeatureToggle[]> {
 | 
				
			||||||
        const query = this.db<FeaturesTable>(TABLE).orderBy('name', 'asc');
 | 
					        const query = this.db<FeaturesTable>(TABLE).orderBy('name', 'asc');
 | 
				
			||||||
        query.whereIn('name', names);
 | 
					        query.whereIn('name', names);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const rows = await query;
 | 
					        const rows = await query;
 | 
				
			||||||
        return rows.map(this.rowToFeature);
 | 
					        return rows.map(this.rowToFeature);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -18,7 +18,7 @@ export const exportQuerySchema = {
 | 
				
			|||||||
    type: 'object',
 | 
					    type: 'object',
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
        'Available query parameters for  the [deprecated export/import](https://docs.getunleash.io/reference/deploy/import-export) functionality.',
 | 
					        'Available query parameters for  the [deprecated export/import](https://docs.getunleash.io/reference/deploy/import-export) functionality.',
 | 
				
			||||||
    oneOf: [
 | 
					    anyOf: [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            required: ['environment', 'features'],
 | 
					            required: ['environment', 'features'],
 | 
				
			||||||
            properties: {
 | 
					            properties: {
 | 
				
			||||||
@ -30,7 +30,8 @@ export const exportQuerySchema = {
 | 
				
			|||||||
                        type: 'string',
 | 
					                        type: 'string',
 | 
				
			||||||
                        minLength: 1,
 | 
					                        minLength: 1,
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                    description: 'Selects features to export by name.',
 | 
					                    description:
 | 
				
			||||||
 | 
					                        'Selects features to export by name. If the list is empty all features are returned.',
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
@ -41,8 +42,19 @@ export const exportQuerySchema = {
 | 
				
			|||||||
                tag: {
 | 
					                tag: {
 | 
				
			||||||
                    type: 'string',
 | 
					                    type: 'string',
 | 
				
			||||||
                    example: 'release',
 | 
					                    example: 'release',
 | 
				
			||||||
 | 
					                    description: 'Selects features to export by tag.',
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            required: ['environment', 'project'],
 | 
				
			||||||
 | 
					            properties: {
 | 
				
			||||||
 | 
					                ...commonProps,
 | 
				
			||||||
 | 
					                project: {
 | 
				
			||||||
 | 
					                    type: 'string',
 | 
				
			||||||
 | 
					                    example: 'my-project',
 | 
				
			||||||
                    description:
 | 
					                    description:
 | 
				
			||||||
                        'Selects features to export by tag. Takes precedence over the features field.',
 | 
					                        'Selects project to export the features from. Used when no tags or features are provided.',
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user