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 { formatUnknownError } from 'utils/formatUnknownError';
import { ConditionallyRender } from '../../common/ConditionallyRender/ConditionallyRender';
interface IExportDialogProps {
showExportDialog: boolean;
data: Pick<FeatureSchema, 'name'>[];
project?: string;
onClose: () => void;
onConfirm?: () => void;
environments: string[];
@ -24,6 +26,7 @@ const StyledSelect = styled(GeneralSelect)(({ theme }) => ({
export const ExportDialog = ({
showExportDialog,
data,
project,
onClose,
onConfirm,
environments,
@ -63,6 +66,7 @@ export const ExportDialog = ({
const payload = {
features: data.map((feature) => feature.name),
environment: selected,
project,
};
const res = await createExport(payload);
const body = await res.json();
@ -84,9 +88,23 @@ export const ExportDialog = ({
secondaryButtonText='Cancel'
>
<Box ref={ref}>
The current search filter will be used to export feature
toggles. Currently {data.length} feature toggles will be
exported.
<ConditionallyRender
condition={data.length > 0}
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 />
<Typography>

View File

@ -42,7 +42,6 @@ export const ProjectFeatureTogglesHeader: VFC<
totalItems,
searchQuery,
onChangeSearchQuery,
dataToExport,
environmentsToExport,
actions,
}) => {
@ -100,7 +99,7 @@ export const ProjectFeatureTogglesHeader: VFC<
show={
<>
<Tooltip
title='Export toggles visible in the table below'
title='Export all project toggles'
arrow
>
<IconButton
@ -123,7 +122,8 @@ export const ProjectFeatureTogglesHeader: VFC<
showExportDialog={
showExportDialog
}
data={dataToExport || []}
project={projectId}
data={[]}
onClose={() =>
setShowExportDialog(false)
}

View File

@ -786,10 +786,21 @@ export default class ExportImportService
userName: string,
userId: number,
): Promise<ExportResultSchema> {
const featureNames =
typeof query.tag === 'string'
? await this.featureTagService.listFeatures(query.tag)
: (query.features as string[]) || [];
let featureNames: string[] = [];
if (typeof query.tag === 'string') {
featureNames = await this.featureTagService.listFeatures(query.tag);
} 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 [
features,
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 createToggle({
name: defaultFeatureName,
@ -567,7 +567,41 @@ test('returns no features, when no feature was requested', async () => {
.set('Content-Type', 'application/json')
.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 = [

View File

@ -305,6 +305,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
async getAllByNames(names: string[]): Promise<FeatureToggle[]> {
const query = this.db<FeaturesTable>(TABLE).orderBy('name', 'asc');
query.whereIn('name', names);
const rows = await query;
return rows.map(this.rowToFeature);
}

View File

@ -18,7 +18,7 @@ export const exportQuerySchema = {
type: 'object',
description:
'Available query parameters for the [deprecated export/import](https://docs.getunleash.io/reference/deploy/import-export) functionality.',
oneOf: [
anyOf: [
{
required: ['environment', 'features'],
properties: {
@ -30,7 +30,8 @@ export const exportQuerySchema = {
type: 'string',
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: {
type: 'string',
example: 'release',
description: 'Selects features to export by tag.',
},
},
},
{
required: ['environment', 'project'],
properties: {
...commonProps,
project: {
type: 'string',
example: 'my-project',
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.',
},
},
},