mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-09-17 13:37:26 +03:00
Add Feature Flags section in Settings
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4018 GitOrigin-RevId: 418fe0c10b5f1ce17a984399a3f773a9a76f83ea
This commit is contained in:
parent
e79aa185f9
commit
121f183231
@ -161,7 +161,7 @@ function:
|
||||
- console: disable search indexing with HTML meta tag
|
||||
- cli: fix inherited roles metadata not being updated when dropping all roles (#7872)
|
||||
- cli: add support for customization field in sources metadata (#8292)
|
||||
- ci: ubuntu and centos flavoured graphql-engine images are now available
|
||||
- console: add feature flags section in settings
|
||||
|
||||
## v2.4.0-beta.2
|
||||
|
||||
|
@ -71,3 +71,5 @@ export {
|
||||
|
||||
export * from './table';
|
||||
export { ReactQueryProvider, reactQueryClient } from '../src/lib/reactQuery';
|
||||
|
||||
export { FeatureFlags } from '../src/features/FeatureFlags'
|
109
console/package-lock.json
generated
109
console/package-lock.json
generated
@ -4903,6 +4903,55 @@
|
||||
"@babel/runtime": "^7.13.10"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-label": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-0.1.5.tgz",
|
||||
"integrity": "sha512-Au9+n4/DhvjR0IHhvZ1LPdx/OW+3CGDie30ZyCkbSHIuLp4/CV4oPPGBwJ1vY99Jog3zyQhsGww9MXj8O9Aj/A==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "0.1.0",
|
||||
"@radix-ui/react-context": "0.1.1",
|
||||
"@radix-ui/react-id": "0.1.5",
|
||||
"@radix-ui/react-primitive": "0.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-context": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-0.1.1.tgz",
|
||||
"integrity": "sha512-PkyVX1JsLBioeu0jB9WvRpDBBLtLZohVDT3BB5CTSJqActma8S8030P57mWZb4baZifMvN7KKWPAA40UmWKkQg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-id": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-0.1.5.tgz",
|
||||
"integrity": "sha512-IPc4H/63bes0IZ1GJJozSEkSWcDyhNGtKFWUpJ+XtaLyQ1X3x7Mf6fWwWhDcpqlYEP+5WtAvfqcyEsyjP+ZhBQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-use-layout-effect": "0.1.0"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-primitive": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-0.1.4.tgz",
|
||||
"integrity": "sha512-6gSl2IidySupIMJFjYnDIkIWRyQdbu/AHK7rbICPani+LW4b0XdxBXc46og/iZvuwW8pjCS8I2SadIerv84xYA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-slot": "0.1.2"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-slot": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-0.1.2.tgz",
|
||||
"integrity": "sha512-ADkqfL+agEzEguU3yS26jfB50hRrwf7U4VTwAOZEmi/g+ITcBWe12yM46ueS/UCIMI9Py+gFUaAdxgxafFvY2Q==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "0.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-popper": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-0.1.0.tgz",
|
||||
@ -4957,6 +5006,66 @@
|
||||
"@radix-ui/react-compose-refs": "0.1.0"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-switch": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-0.1.5.tgz",
|
||||
"integrity": "sha512-ITtslJPK+Yi34iNf7K9LtsPaLD76oRIVzn0E8JpEO5HW8gpRBGb2NNI9mxKtEB30TVqIcdjdL10AmuIfOMwjtg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "0.1.0",
|
||||
"@radix-ui/react-compose-refs": "0.1.0",
|
||||
"@radix-ui/react-context": "0.1.1",
|
||||
"@radix-ui/react-label": "0.1.5",
|
||||
"@radix-ui/react-primitive": "0.1.4",
|
||||
"@radix-ui/react-use-controllable-state": "0.1.0",
|
||||
"@radix-ui/react-use-previous": "0.1.1",
|
||||
"@radix-ui/react-use-size": "0.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-context": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-0.1.1.tgz",
|
||||
"integrity": "sha512-PkyVX1JsLBioeu0jB9WvRpDBBLtLZohVDT3BB5CTSJqActma8S8030P57mWZb4baZifMvN7KKWPAA40UmWKkQg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-primitive": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-0.1.4.tgz",
|
||||
"integrity": "sha512-6gSl2IidySupIMJFjYnDIkIWRyQdbu/AHK7rbICPani+LW4b0XdxBXc46og/iZvuwW8pjCS8I2SadIerv84xYA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-slot": "0.1.2"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-slot": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-0.1.2.tgz",
|
||||
"integrity": "sha512-ADkqfL+agEzEguU3yS26jfB50hRrwf7U4VTwAOZEmi/g+ITcBWe12yM46ueS/UCIMI9Py+gFUaAdxgxafFvY2Q==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "0.1.0"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-use-previous": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-0.1.1.tgz",
|
||||
"integrity": "sha512-O/ZgrDBr11dR8rhO59ED8s5zIXBRFi8MiS+CmFGfi7MJYdLbfqVOmQU90Ghf87aifEgWe6380LA69KBneaShAg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-use-size": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-0.1.1.tgz",
|
||||
"integrity": "sha512-pTgWM5qKBu6C7kfKxrKPoBI2zZYZmp2cSXzpUiGM3qEBQlMLtYhaY2JXdXUCxz+XmD1YEjc8oRwvyfsD4AG4WA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-tooltip": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-0.1.0.tgz",
|
||||
|
@ -61,6 +61,7 @@
|
||||
"@graphql-codegen/typescript": "^1.17.10",
|
||||
"@hookform/resolvers": "^2.8.1",
|
||||
"@radix-ui/react-collapsible": "^0.1.1",
|
||||
"@radix-ui/react-switch": "^0.1.5",
|
||||
"@radix-ui/react-tooltip": "^0.1.0",
|
||||
"@reduxjs/toolkit": "^1.5.1",
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
|
@ -31,7 +31,8 @@ type SectionDataKey =
|
||||
| 'logout'
|
||||
| 'about'
|
||||
| 'inherited-roles'
|
||||
| 'insecure-domain';
|
||||
| 'insecure-domain'
|
||||
| 'feature-flags';
|
||||
|
||||
interface SectionData {
|
||||
key: SectionDataKey;
|
||||
@ -114,6 +115,13 @@ const Sidebar: React.FC<SidebarProps> = ({ location, metadata }) => {
|
||||
title: 'Insecure TLS Allow List',
|
||||
});
|
||||
|
||||
sectionsData.push({
|
||||
key: 'feature-flags',
|
||||
link: '/settings/feature-flags',
|
||||
dataTestVal: 'feature-flags-link',
|
||||
title: 'Feature Flags',
|
||||
});
|
||||
|
||||
const currentLocation = location.pathname;
|
||||
|
||||
const sections: JSX.Element[] = [];
|
||||
|
@ -0,0 +1,10 @@
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
import React from 'react';
|
||||
import { FeatureFlagFloatingButton } from './FeatureFlagFloatingButton';
|
||||
|
||||
export default {
|
||||
title: 'feature/FeatureFlags/FeatureFlagFloatingButton',
|
||||
component: FeatureFlagFloatingButton,
|
||||
} as ComponentMeta<typeof FeatureFlagFloatingButton>;
|
||||
|
||||
export const Main = () => <FeatureFlagFloatingButton />;
|
@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { FaFlask } from 'react-icons/fa';
|
||||
|
||||
export const FeatureFlagFloatingButton = () => {
|
||||
return (
|
||||
<button className="fixed flex items-center justify-center bottom-4 right-4 bg-white border overflow-hidden shadow-xl rounded-lg font-sans w-12 h-12">
|
||||
<FaFlask />
|
||||
</button>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export { FeatureFlagFloatingButton } from './FeatureFlagFloatingButton';
|
@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import { useFeatureFlagDismiss } from '../../hooks/useFeatureFlagDismiss';
|
||||
import { useFeatureFlags } from '../../hooks/useFeatureFlags';
|
||||
import { FeatureFlagDefinition, FeatureFlagId } from '../../types';
|
||||
import _push from '../../../../components/Services/Data/push';
|
||||
|
||||
interface FeatureFlagToastProps {
|
||||
flagId: FeatureFlagId;
|
||||
additionalFlags?: FeatureFlagDefinition[];
|
||||
}
|
||||
|
||||
export const FeatureFlagToast = (props: FeatureFlagToastProps) => {
|
||||
const { flagId, additionalFlags } = props;
|
||||
const [dismissed, setDismissed] = React.useState(false);
|
||||
const { isError, isLoading, data } = useFeatureFlags(additionalFlags);
|
||||
const setPermanentDismiss = useFeatureFlagDismiss();
|
||||
const featureFlag = data?.find(i => i.id === flagId);
|
||||
if (
|
||||
isError ||
|
||||
isLoading ||
|
||||
dismissed ||
|
||||
!featureFlag ||
|
||||
featureFlag?.state.dismissed
|
||||
)
|
||||
return null;
|
||||
return (
|
||||
<div className="fixed bottom-8 right-8 bg-white border overflow-hidden shadow-xl rounded-lg w-px-320 font-sans">
|
||||
<div className="bg-primary px-4 py-3 flex align-middle">
|
||||
<h3 className="text-lg font-bold mb-0">
|
||||
Coming Soon: {featureFlag?.title}
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
className="flex p-4 cursor-pointer items-center justify-between"
|
||||
onClick={() => _push('/settings/feature-flags')}
|
||||
>
|
||||
Try out the new feature before it gets to general availability.
|
||||
<i className="fa fa-chevron-right ml-2" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="flex p-4 justify-between border-t">
|
||||
<button onClick={() => setDismissed(true)}>Hide for now</button>
|
||||
<button
|
||||
className="text-gray-400"
|
||||
onClick={() => setPermanentDismiss.mutate(featureFlag?.id ?? '')}
|
||||
>
|
||||
Don't show me again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
|
||||
import { FeatureFlagToast } from './FeatureFlagToast';
|
||||
import { FeatureFlagDefinition } from '../../types';
|
||||
|
||||
export default {
|
||||
title: 'feature/FeatureFlags/FeatureFlagToast',
|
||||
component: FeatureFlagToast,
|
||||
decorators: [ReactQueryDecorator()],
|
||||
} as ComponentMeta<typeof FeatureFlagToast>;
|
||||
|
||||
const additionalFlags: FeatureFlagDefinition[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Database Table → Remote Schema Relationship',
|
||||
description:
|
||||
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.",
|
||||
section: 'api',
|
||||
status: 'alpha',
|
||||
defaultValue: false,
|
||||
discussionUrl: '',
|
||||
},
|
||||
];
|
||||
|
||||
export const Main = () => (
|
||||
<FeatureFlagToast flagId="1" additionalFlags={additionalFlags} />
|
||||
);
|
@ -0,0 +1 @@
|
||||
export { FeatureFlagToast } from './FeatureFlagToast';
|
@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
|
||||
import { FeatureFlags } from './FeatureFlags';
|
||||
import { FeatureFlagDefinition } from '../../types';
|
||||
|
||||
export default {
|
||||
title: 'feature/FeatureFlags',
|
||||
component: FeatureFlags,
|
||||
decorators: [ReactQueryDecorator()],
|
||||
} as ComponentMeta<typeof FeatureFlags>;
|
||||
|
||||
const additionalFlags: FeatureFlagDefinition[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Database Table → Remote Schema Relationship',
|
||||
description:
|
||||
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.",
|
||||
section: 'api',
|
||||
status: 'alpha',
|
||||
defaultValue: false,
|
||||
discussionUrl: '',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Database Table → Remote Schema Relationship',
|
||||
description:
|
||||
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.",
|
||||
section: 'api',
|
||||
status: 'alpha',
|
||||
defaultValue: true,
|
||||
discussionUrl: '',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Database Table → Remote Schema Relationship',
|
||||
description:
|
||||
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.",
|
||||
section: 'api',
|
||||
status: 'alpha',
|
||||
defaultValue: false,
|
||||
discussionUrl: '',
|
||||
},
|
||||
];
|
||||
|
||||
export const Main = () => <FeatureFlags additionalFlags={additionalFlags} />;
|
||||
|
||||
export const EmptyState = () => <FeatureFlags />;
|
@ -0,0 +1,84 @@
|
||||
import { CardedTable } from '@/new-components/CardedTable';
|
||||
import { Switch } from '@/new-components/Switch';
|
||||
import React from 'react';
|
||||
import { useFeatureFlags } from '../../hooks/useFeatureFlags';
|
||||
import { useSetFeatureFlagEnabled } from '../../hooks/useSetFeatureFlagEnabled';
|
||||
import { FeatureFlagDefinition, FeatureFlagType } from '../../types';
|
||||
|
||||
interface FeatureCellProps {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const FeatureCell = (props: FeatureCellProps) => {
|
||||
const { description, title } = props;
|
||||
return (
|
||||
<div className="pr-4">
|
||||
<div className="text-gray-600 font-semibold break-all">{title}</div>
|
||||
<div className="text-gray-600 whitespace-normal">{description}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const columns = [null, 'Feature', 'section', 'status'];
|
||||
|
||||
const formatData = (
|
||||
data: Array<FeatureFlagType>,
|
||||
mutation: ReturnType<typeof useSetFeatureFlagEnabled>
|
||||
): React.ReactNode[][] =>
|
||||
data.map(item => [
|
||||
<Switch
|
||||
checked={item.state.enabled}
|
||||
onCheckedChange={() =>
|
||||
mutation.mutate({ flagId: item.id, newState: !item.state.enabled })
|
||||
}
|
||||
/>,
|
||||
<FeatureCell title={item.title} description={item.description} />,
|
||||
item.section,
|
||||
item.status,
|
||||
]);
|
||||
|
||||
interface FeatureFlagsProps {
|
||||
additionalFlags?: FeatureFlagDefinition[];
|
||||
}
|
||||
|
||||
const FeatureFlagsBody = (props: FeatureFlagsProps) => {
|
||||
const { additionalFlags } = props;
|
||||
const result = useFeatureFlags(additionalFlags);
|
||||
const setFeatureFlagEnabled = useSetFeatureFlagEnabled();
|
||||
const { isError, isLoading, data } = result;
|
||||
if (isLoading) return <h3 className="text-lg">Loading...</h3>;
|
||||
if (isError)
|
||||
return (
|
||||
<h3 className="text-lg">
|
||||
There was an error while loading. <br />
|
||||
Please try again later.
|
||||
</h3>
|
||||
);
|
||||
if (data && data?.length > 0) {
|
||||
return (
|
||||
<CardedTable
|
||||
columns={columns}
|
||||
data={formatData(data, setFeatureFlagEnabled)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<h3 className="text-lg">There are currently no Feature flags available.</h3>
|
||||
);
|
||||
};
|
||||
|
||||
export const FeatureFlags = (props: FeatureFlagsProps) => {
|
||||
const { additionalFlags } = props;
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h2 className="text-xl font-semibold mb-3.5">Feature Flags</h2>
|
||||
<p>
|
||||
Feature flags enable experimental features in Console. <br />
|
||||
These features may be actively in development or in beta status.
|
||||
</p>
|
||||
<div className="mb-10" />
|
||||
<FeatureFlagsBody additionalFlags={additionalFlags} />
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export { FeatureFlags } from './FeatureFlags';
|
@ -15,7 +15,7 @@ describe('mergeFlagWithState', () => {
|
||||
},
|
||||
];
|
||||
const inputState: FeatureFlagState[] = [
|
||||
{ id: 'AAA', dismissed: false, enable: true },
|
||||
{ id: 'AAA', dismissed: false, enabled: true },
|
||||
];
|
||||
|
||||
const result = mergeFlagWithState(inputFlags, inputState);
|
||||
@ -30,7 +30,7 @@ describe('mergeFlagWithState', () => {
|
||||
"section": "api",
|
||||
"state": Object {
|
||||
"dismissed": false,
|
||||
"enable": true,
|
||||
"enabled": true,
|
||||
"id": "AAA",
|
||||
},
|
||||
"status": "beta",
|
||||
@ -53,7 +53,7 @@ describe('mergeFlagWithState', () => {
|
||||
},
|
||||
];
|
||||
const inputState: FeatureFlagState[] = [
|
||||
{ id: 'CCC', dismissed: false, enable: true },
|
||||
{ id: 'CCC', dismissed: false, enabled: true },
|
||||
];
|
||||
|
||||
const result = mergeFlagWithState(inputFlags, inputState);
|
||||
@ -68,7 +68,7 @@ describe('mergeFlagWithState', () => {
|
||||
"section": "api",
|
||||
"state": Object {
|
||||
"dismissed": false,
|
||||
"enable": false,
|
||||
"enabled": false,
|
||||
},
|
||||
"status": "beta",
|
||||
"title": "Hello world",
|
||||
@ -112,7 +112,7 @@ describe('mergeFlagWithState', () => {
|
||||
"section": "api",
|
||||
"state": Object {
|
||||
"dismissed": false,
|
||||
"enable": true,
|
||||
"enabled": true,
|
||||
},
|
||||
"status": "beta",
|
||||
"title": "Hello world",
|
||||
@ -125,7 +125,7 @@ describe('mergeFlagWithState', () => {
|
||||
"section": "api",
|
||||
"state": Object {
|
||||
"dismissed": false,
|
||||
"enable": false,
|
||||
"enabled": false,
|
||||
},
|
||||
"status": "beta",
|
||||
"title": "Hello world",
|
@ -1,5 +1,5 @@
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { FeatureFlagId, useFeatureFlags } from '@/features/FeatureFlag';
|
||||
import { FeatureFlagId, useFeatureFlags } from '@/features/FeatureFlags';
|
||||
import { saveFeatureFlagsStateToLocalStorage } from '../utils';
|
||||
|
||||
export function useFeatureFlagDismiss() {
|
@ -33,18 +33,20 @@ export const mergeFlagWithState = (
|
||||
return {
|
||||
...flag,
|
||||
state: flagState ?? {
|
||||
enable: flag.defaultValue,
|
||||
enabled: flag.defaultValue,
|
||||
dismissed: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export function useFeatureFlags() {
|
||||
export function useFeatureFlags(additionalFlags?: FeatureFlagDefinition[]) {
|
||||
return useQuery(['featureFlags', 'all'], () =>
|
||||
Promise.all([
|
||||
getAvailableFeatureFlags(),
|
||||
getFeatureFlagStore(),
|
||||
]).then(([flags, state]) => mergeFlagWithState(flags, state))
|
||||
]).then(([flags, state]) =>
|
||||
mergeFlagWithState([...(additionalFlags ?? []), ...flags], state)
|
||||
)
|
||||
);
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { FeatureFlagId, useFeatureFlags } from '@/features/FeatureFlag';
|
||||
import { FeatureFlagId, useFeatureFlags } from '@/features/FeatureFlags';
|
||||
import { saveFeatureFlagsStateToLocalStorage } from '../utils';
|
||||
|
||||
export function useFeatureFlagDismiss() {
|
||||
export function useSetFeatureFlagEnabled() {
|
||||
const queryClient = useQueryClient();
|
||||
const { data, isError, isLoading } = useFeatureFlags();
|
||||
|
||||
@ -25,7 +25,7 @@ export function useFeatureFlagDismiss() {
|
||||
...item,
|
||||
state: {
|
||||
...item.state,
|
||||
enable: newState,
|
||||
enabled: newState,
|
||||
enableDate: new Date(),
|
||||
},
|
||||
};
|
@ -1,2 +1,4 @@
|
||||
export { FeatureFlags } from './components/FeatureFlags';
|
||||
export { FeatureFlagType, FeatureFlagSections, FeatureFlagId } from './types';
|
||||
export { useFeatureFlags } from './hooks/useFeatureFlags';
|
||||
export { FeatureFlagToast } from './components/FeatureFlagToast';
|
@ -23,7 +23,7 @@ export interface FeatureFlagDefinition {
|
||||
|
||||
export interface FeatureFlagState {
|
||||
id: FeatureFlagId;
|
||||
enable: boolean;
|
||||
enabled: boolean;
|
||||
enableDate?: Date;
|
||||
dismissed: boolean;
|
||||
dismissedDate?: Date;
|
17
console/src/new-components/Switch/Switch.stories.tsx
Normal file
17
console/src/new-components/Switch/Switch.stories.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
import { Switch } from './Switch';
|
||||
|
||||
export default {
|
||||
title: 'components/Switch',
|
||||
component: Switch,
|
||||
} as ComponentMeta<typeof Switch>;
|
||||
|
||||
export const Off = () => <Switch />;
|
||||
|
||||
export const On = () => <Switch checked />;
|
||||
|
||||
export const Playground = () => {
|
||||
const [checked, setChecked] = React.useState(false);
|
||||
return <Switch checked={checked} onCheckedChange={setChecked} />;
|
||||
};
|
23
console/src/new-components/Switch/Switch.tsx
Normal file
23
console/src/new-components/Switch/Switch.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import * as RadixSwitch from '@radix-ui/react-switch';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export const Switch = (props: RadixSwitch.SwitchProps) => {
|
||||
const { checked } = props;
|
||||
return (
|
||||
<RadixSwitch.Root
|
||||
className={clsx(
|
||||
checked ? 'bg-green-600' : 'bg-gray-200',
|
||||
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500'
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadixSwitch.Thumb
|
||||
className={clsx(
|
||||
checked ? 'translate-x-5' : 'translate-x-0',
|
||||
'pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||
)}
|
||||
/>
|
||||
</RadixSwitch.Root>
|
||||
);
|
||||
};
|
1
console/src/new-components/Switch/index.tsx
Normal file
1
console/src/new-components/Switch/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { Switch } from './Switch';
|
@ -37,6 +37,7 @@ import {
|
||||
INSECURE_TLS_ALLOW_LIST,
|
||||
} from './helpers/versionUtils';
|
||||
import AuthContainer from './components/Services/Auth/AuthContainer';
|
||||
import { FeatureFlags } from './features/FeatureFlags';
|
||||
|
||||
const routes = store => {
|
||||
// load hasuractl migration status
|
||||
@ -151,6 +152,7 @@ const routes = store => {
|
||||
{checkFeatureSupport(INSECURE_TLS_ALLOW_LIST) && (
|
||||
<Route path="insecure-domain" component={InsecureDomains} />
|
||||
)}
|
||||
<Route path="feature-flags" component={FeatureFlags} />
|
||||
</Route>
|
||||
{dataRouter}
|
||||
{remoteSchemaRouter}
|
||||
|
Loading…
Reference in New Issue
Block a user