1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-31 00:16:47 +01:00

feat: add import export flag (#3411)

This commit is contained in:
Jaanus Sellin 2023-03-29 09:19:33 +03:00 committed by GitHub
parent fec4ed1b15
commit 99bcd7ca5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 115 additions and 49 deletions

View File

@ -68,6 +68,7 @@ export const FeatureToggleListTable: VFC = () => {
const [showExportDialog, setShowExportDialog] = useState(false);
const { features = [], loading, refetchFeatures } = useFeatures();
const [searchParams, setSearchParams] = useSearchParams();
const { uiConfig } = useUiConfig();
const [initialState] = useState(() => ({
sortBy: [
{
@ -310,16 +311,28 @@ export const FeatureToggleListTable: VFC = () => {
>
View archive
</Link>
<Tooltip title="Export current selection" arrow>
<IconButton
onClick={() => setShowExportDialog(true)}
sx={theme => ({
marginRight: theme.spacing(2),
})}
>
<FileDownload />
</IconButton>
</Tooltip>
<ConditionallyRender
condition={Boolean(
uiConfig?.flags?.featuresExportImport
)}
show={
<Tooltip
title="Export current selection"
arrow
>
<IconButton
onClick={() =>
setShowExportDialog(true)
}
sx={theme => ({
marginRight: theme.spacing(2),
})}
>
<FileDownload />
</IconButton>
</Tooltip>
}
/>
<CreateFeatureButton
loading={false}
@ -370,11 +383,16 @@ export const FeatureToggleListTable: VFC = () => {
/>
}
/>
<ExportDialog
showExportDialog={showExportDialog}
data={data}
onClose={() => setShowExportDialog(false)}
environments={enabledEnvironments}
<ConditionallyRender
condition={Boolean(uiConfig?.flags?.featuresExportImport)}
show={
<ExportDialog
showExportDialog={showExportDialog}
data={data}
onClose={() => setShowExportDialog(false)}
environments={enabledEnvironments}
/>
}
/>
</PageContent>
);

View File

@ -139,16 +139,23 @@ export const Project = () => {
</StyledProjectTitle>
</StyledDiv>
<StyledDiv>
<PermissionIconButton
permission={UPDATE_FEATURE}
projectId={projectId}
onClick={() => setModalOpen(true)}
tooltipProps={{ title: 'Import' }}
data-testid={IMPORT_BUTTON}
data-loading
>
<FileUpload />
</PermissionIconButton>
<ConditionallyRender
condition={Boolean(
uiConfig?.flags?.featuresExportImport
)}
show={
<PermissionIconButton
permission={UPDATE_FEATURE}
projectId={projectId}
onClick={() => setModalOpen(true)}
tooltipProps={{ title: 'Import' }}
data-testid={IMPORT_BUTTON}
data-loading
>
<FileUpload />
</PermissionIconButton>
}
/>
<ConditionallyRender
condition={!isOss()}
show={

View File

@ -583,21 +583,29 @@ export const ProjectFeatureToggles = ({
setHiddenColumns={setHiddenColumns}
/>
<PageHeader.Divider sx={{ marginLeft: 0 }} />
<Tooltip
title="Export toggles visible in the table below"
arrow
>
<IconButton
onClick={() =>
setShowExportDialog(true)
}
sx={theme => ({
marginRight: theme.spacing(2),
})}
>
<FileDownload />
</IconButton>
</Tooltip>
<ConditionallyRender
condition={Boolean(
uiConfig?.flags?.featuresExportImport
)}
show={
<Tooltip
title="Export toggles visible in the table below"
arrow
>
<IconButton
onClick={() =>
setShowExportDialog(true)
}
sx={theme => ({
marginRight:
theme.spacing(2),
})}
>
<FileDownload />
</IconButton>
</Tooltip>
}
/>
<StyledResponsiveButton
onClick={() =>
navigate(getCreateTogglePath(projectId))
@ -704,7 +712,10 @@ export const ProjectFeatureToggles = ({
}
/>
<ConditionallyRender
condition={!loading}
condition={
Boolean(uiConfig?.flags?.featuresExportImport) &&
!loading
}
show={
<ExportDialog
showExportDialog={showExportDialog}

View File

@ -4,6 +4,7 @@ import { FileDownload } from '@mui/icons-material';
import type { FeatureSchema } from 'openapi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { ArchiveButton } from './ArchiveButton';
import { MoreActions } from './MoreActions';
import { ManageTags } from './ManageTags';
@ -18,6 +19,7 @@ interface IProjectFeaturesBatchActionsProps {
export const ProjectFeaturesBatchActions: FC<
IProjectFeaturesBatchActionsProps
> = ({ selectedIds, data, projectId }) => {
const { uiConfig } = useUiConfig();
const [showExportDialog, setShowExportDialog] = useState(false);
const { trackEvent } = usePlausibleTracker();
const selectedData = useMemo(
@ -54,12 +56,17 @@ export const ProjectFeaturesBatchActions: FC<
</Button>
<ManageTags projectId={projectId} data={selectedData} />
<MoreActions projectId={projectId} data={selectedData} />
<ExportDialog
showExportDialog={showExportDialog}
data={selectedData}
onClose={() => setShowExportDialog(false)}
environments={environments}
onConfirm={trackExport}
<ConditionallyRender
condition={Boolean(uiConfig?.flags?.featuresExportImport)}
show={
<ExportDialog
showExportDialog={showExportDialog}
data={selectedData}
onClose={() => setShowExportDialog(false)}
environments={environments}
onConfirm={trackExport}
/>
}
/>
</>
);

View File

@ -40,6 +40,7 @@ export interface IFlags {
embedProxyFrontend?: boolean;
maintenanceMode?: boolean;
messageBanner?: boolean;
featuresExportImport?: boolean;
newProjectOverview?: boolean;
caseInsensitiveInOperators?: boolean;
crOnVariants?: boolean;

View File

@ -75,6 +75,7 @@ exports[`should create default config 1`] = `
"crOnVariants": false,
"embedProxy": true,
"embedProxyFrontend": true,
"featuresExportImport": true,
"loginHistory": false,
"maintenanceMode": false,
"messageBanner": false,
@ -101,6 +102,7 @@ exports[`should create default config 1`] = `
"crOnVariants": false,
"embedProxy": true,
"embedProxyFrontend": true,
"featuresExportImport": true,
"loginHistory": false,
"maintenanceMode": false,
"messageBanner": false,

View File

@ -21,6 +21,7 @@ import {
} from '../../openapi';
import { IAuthRequest } from '../../routes/unleash-types';
import { extractUsername } from '../../util';
import { InvalidOperationError } from '../../error';
class ExportImportController extends Controller {
private logger: Logger;
@ -118,6 +119,7 @@ class ExportImportController extends Controller {
req: IAuthRequest<unknown, unknown, ExportQuerySchema, unknown>,
res: Response,
): Promise<void> {
this.verifyExportImportEnabled();
const query = req.body;
const userName = extractUsername(req);
const data = await this.exportImportService.export(query, userName);
@ -134,6 +136,7 @@ class ExportImportController extends Controller {
req: IAuthRequest<unknown, unknown, ImportTogglesSchema, unknown>,
res: Response,
): Promise<void> {
this.verifyExportImportEnabled();
const dto = req.body;
const { user } = req;
const validation = await this.startTransaction(async (tx) =>
@ -152,6 +155,7 @@ class ExportImportController extends Controller {
req: IAuthRequest<unknown, unknown, ImportTogglesSchema, unknown>,
res: Response,
): Promise<void> {
this.verifyExportImportEnabled();
const dto = req.body;
const { user } = req;
await this.startTransaction(async (tx) =>
@ -160,5 +164,13 @@ class ExportImportController extends Controller {
res.status(200).end();
}
private verifyExportImportEnabled() {
if (!this.config.flagResolver.isEnabled('featuresExportImport')) {
throw new InvalidOperationError(
'Feature export/import is not enabled',
);
}
}
}
export default ExportImportController;

View File

@ -233,7 +233,9 @@ beforeAll(async () => {
db.stores,
{
experimental: {
flags: {},
flags: {
featuresExportImport: true,
},
},
},
db.rawDatabase,

View File

@ -138,7 +138,9 @@ beforeAll(async () => {
db.stores,
{
experimental: {
flags: {},
flags: {
featuresExportImport: true,
},
},
},
db.rawDatabase,

View File

@ -34,6 +34,10 @@ const flags = {
process.env.UNLEASH_EXPERIMENTAL_MESSAGE_BANNER,
false,
),
featuresExportImport: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_FEATURES_EXPORT_IMPORT,
true,
),
caseInsensitiveInOperators: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_CASE_INSENSITIVE_IN_OPERATORS,
false,