1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

feat: export all features in project (#5677)

This commit is contained in:
Mateusz Kwasniewski 2023-12-19 08:57:10 +01:00 committed by GitHub
parent 1043efd89f
commit 7800d9d1b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 91 additions and 15 deletions

View File

@ -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>

View File

@ -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)
} }

View File

@ -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,

View File

@ -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 = [

View File

@ -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);
} }

View File

@ -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.',
}, },
}, },
}, },