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