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:
parent
fec4ed1b15
commit
99bcd7ca5c
@ -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>
|
||||
);
|
||||
|
@ -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={
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -40,6 +40,7 @@ export interface IFlags {
|
||||
embedProxyFrontend?: boolean;
|
||||
maintenanceMode?: boolean;
|
||||
messageBanner?: boolean;
|
||||
featuresExportImport?: boolean;
|
||||
newProjectOverview?: boolean;
|
||||
caseInsensitiveInOperators?: boolean;
|
||||
crOnVariants?: boolean;
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -233,7 +233,9 @@ beforeAll(async () => {
|
||||
db.stores,
|
||||
{
|
||||
experimental: {
|
||||
flags: {},
|
||||
flags: {
|
||||
featuresExportImport: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
db.rawDatabase,
|
||||
|
@ -138,7 +138,9 @@ beforeAll(async () => {
|
||||
db.stores,
|
||||
{
|
||||
experimental: {
|
||||
flags: {},
|
||||
flags: {
|
||||
featuresExportImport: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
db.rawDatabase,
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user