Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
101243c
initial front end feature flag implementation
pavolumMsft Oct 6, 2020
912d101
Merging latest
pavolumMsft Oct 13, 2020
a1d73da
Initial server implementation
pavolumMsft Oct 16, 2020
a788f58
Converting state to object over array
pavolumMsft Oct 16, 2020
239a45b
Cleaning up dev test changes
pavolumMsft Oct 16, 2020
19a1561
created HOC to abstract feature flag checking for UI features. Change…
pavolumMsft Oct 19, 2020
f65cf8a
Added feature flag value population for hidden feature flags via env …
pavolumMsft Oct 20, 2020
7e24fce
Making PR changes: variable and file name changes, general clean up, …
pavolumMsft Oct 20, 2020
8a4684e
adding format message for feature flag strings
pavolumMsft Oct 20, 2020
bb97679
moving ComposerFeature HOC to client workspace so it is implicitly aw…
pavolumMsft Oct 20, 2020
ecf4f25
changed default feature flags to function for format message, created…
pavolumMsft Oct 21, 2020
76008bf
merging latest
pavolumMsft Oct 21, 2020
08da677
adding selector for feature flag filtered template state, created fea…
pavolumMsft Oct 22, 2020
f1ec8c8
removing unneeded div
pavolumMsft Oct 23, 2020
7f1b773
Adding util functions and reusing hook in HOC
pavolumMsft Oct 23, 2020
45ae235
Updating UI for electron and web view of feature flag toggle per desi…
pavolumMsft Oct 23, 2020
5c4373a
Added feature flag documentation
pavolumMsft Oct 23, 2020
3bf1f67
Merging latest
pavolumMsft Oct 23, 2020
be7db2a
removing unused imports and variables
pavolumMsft Oct 23, 2020
1101fe5
Updated unit tests, added build locale
pavolumMsft Oct 24, 2020
7f5511f
Merge branch 'main' into pavolum/featureFlags
cwhitten Oct 24, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Composer/packages/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ initializeIcons(undefined, { disableWarnings: true });

export const App: React.FC = () => {
const { appLocale } = useRecoilValue(userSettingsState);
const { fetchFeatureFlags } = useRecoilValue(dispatcherState);
useEffect(() => {
loadLocale(appLocale);
}, [appLocale]);

const { fetchExtensions } = useRecoilValue(dispatcherState);
useEffect(() => {
fetchExtensions();
fetchFeatureFlags();
});

return (
Expand Down
19 changes: 19 additions & 0 deletions Composer/packages/client/src/components/ComposerFeature.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { FeatureFlagKey } from '@bfc/shared';
import React, { Fragment } from 'react';
import { useRecoilValue } from 'recoil';

import { featureFlagsState } from '../recoilModel';

type ComposerFeatureProps = {
featureFlagKey: FeatureFlagKey;
};

export const ComposerFeature: React.FC<ComposerFeatureProps> = (props) => {
const { featureFlagKey } = props;
const featureFlags = useRecoilValue(featureFlagsState);
return (
<Fragment>{featureFlags[featureFlagKey] && featureFlags[featureFlagKey].enabled ? props.children : null}</Fragment>
);
};
3 changes: 2 additions & 1 deletion Composer/packages/client/src/pages/home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
templateProjectsState,
templateIdState,
currentProjectIdState,
featureFlagsState,
} from '../../recoilModel/atoms/appState';
import { Toolbar, IToolbarItem } from '../../components/Toolbar';

Expand Down Expand Up @@ -66,6 +67,7 @@ const Home: React.FC<RouteComponentProps> = () => {
const botName = useRecoilValue(botDisplayNameState(projectId));
const recentProjects = useRecoilValue(recentProjectsState);
const templateId = useRecoilValue(templateIdState);
const featureFlagMap = useRecoilValue(featureFlagsState);
const { openProject, setCreationFlowStatus, onboardingAddCoachMarkRef, saveTemplateId } = useRecoilValue(
dispatcherState
);
Expand Down Expand Up @@ -135,7 +137,6 @@ const Home: React.FC<RouteComponentProps> = () => {
disabled: botName ? false : true,
},
];

return (
<div css={home.outline}>
<Toolbar toolbarItems={toolbarItems} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,29 @@ import { DirectionalHint } from 'office-ui-fabric-react/lib/common/DirectionalHi
import { NeutralColors } from '@uifabric/fluent-theme';
import { RouteComponentProps } from '@reach/router';
import { useRecoilValue } from 'recoil';
import { FeatureFlag, FeatureFlagKey } from '@bfc/shared';

import { isElectron } from '../../../utils/electronUtil';
import { onboardingState, userSettingsState, dispatcherState } from '../../../recoilModel';
import { onboardingState, userSettingsState, dispatcherState, featureFlagsState } from '../../../recoilModel';

import { container, section } from './styles';
import { container, featureFlagGroupContainer, section } from './styles';
import { SettingToggle } from './SettingToggle';
import { SettingDropdown } from './SettingDropdown';
import * as images from './images';
import { FeatureFlagCheckBox } from './FeatureFlagCheckBox';

const ElectronSettings = lazy(() =>
import('./electronSettings').then((module) => ({ default: module.ElectronSettings }))
);

const AppSettings: React.FC<RouteComponentProps> = () => {
const [calloutIsShown, showCallout] = useState(false);
const [featureFlagVisible, showFeatureFlag] = useState(false);

const { onboardingSetComplete, updateUserSettings } = useRecoilValue(dispatcherState);
const { onboardingSetComplete, updateUserSettings, toggleFeatureFlag } = useRecoilValue(dispatcherState);
const userSettings = useRecoilValue(userSettingsState);
const { complete } = useRecoilValue(onboardingState);

const featureFlags = useRecoilValue(featureFlagsState);
const onOnboardingChange = useCallback(
(checked: boolean) => {
// on means its not complete
Expand Down Expand Up @@ -62,6 +65,27 @@ const AppSettings: React.FC<RouteComponentProps> = () => {
});
}

const renderFeatureFlagOptions = () => {
const result: React.ReactNode[] = [];
console.log(featureFlags);
Object.keys(featureFlags).forEach((key: string) => {
const featureFlag: FeatureFlag = featureFlags[key];
if (!featureFlag.isHidden) {
result.push(
<FeatureFlagCheckBox
key={key}
description={featureFlag.description}
enabled={featureFlag.enabled}
featureFlagKey={key as FeatureFlagKey}
featureFlagName={featureFlag.displayName}
toggleFeatureFlag={toggleFeatureFlag}
/>
);
}
});
return <div css={featureFlagGroupContainer}>{result}</div>;
};

return (
<div css={container}>
<section css={section}>
Expand Down Expand Up @@ -155,6 +179,20 @@ const AppSettings: React.FC<RouteComponentProps> = () => {
onChange={onLocaleChange}
/>
</section>
<section css={section}>
<h2>{formatMessage('Application Updates')}</h2>
<SettingToggle
hideToggle
checked={featureFlagVisible}
description={formatMessage('Toggle the visibility of individual, preview, features in Composer.')}
image={images.previewFeatures}
title={formatMessage('Preview features')}
onToggle={(checked: boolean) => {
showFeatureFlag(checked);
}}
/>
{renderFeatureFlagOptions()}
</section>
<Suspense fallback={<div />}>{renderElectronSettings && <ElectronSettings />}</Suspense>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/** @jsx jsx */
import { jsx } from '@emotion/core';
import React from 'react';
import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox';
import { FeatureFlagKey } from '@bfc/shared';

import * as styles from './styles';

type FeatureFlagCheckBoxProps = {
featureFlagKey: FeatureFlagKey;
featureFlagName: string;
description: string;
enabled: boolean;
toggleFeatureFlag: (FeatureFlagKey: string, enabled: boolean) => void;
};

const renderLabel = (featureName: string, description: string) => () => (
<span>
<span css={styles.featureFlagTitle}>{`${featureName}.`}</span>
{` ${description}`}
</span>
);

export const FeatureFlagCheckBox: React.FC<FeatureFlagCheckBoxProps> = (props) => {
return (
<Checkbox
checked={props.enabled}
css={styles.featureFlagContainer}
onChange={(e: any, checked?: boolean) => {
if (checked !== undefined) {
props.toggleFeatureFlag(props.featureFlagKey, checked);
}
}}
onRenderLabel={renderLabel(props.featureFlagName, props.description)}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ interface ISettingToggleProps {
image: string;
onToggle: (checked: boolean) => void;
title: string;
hideToggle?: boolean;
}

const SettingToggle: React.FC<ISettingToggleProps> = (props) => {
const { id, title, description, image, checked, onToggle } = props;
const { id, title, description, image, checked, onToggle, hideToggle } = props;
const uniqueId = useId(kebabCase(title));

return (
Expand All @@ -36,16 +37,18 @@ const SettingToggle: React.FC<ISettingToggleProps> = (props) => {
</Label>
<p css={styles.settingsDescription}>{description}</p>
</div>
<div>
<Toggle
checked={!!checked}
data-testid={id}
id={id || uniqueId}
offText={formatMessage('Off')}
onChange={(_e, checked) => onToggle(!!checked)}
onText={formatMessage('On')}
/>
</div>
{!hideToggle && (
<div>
<Toggle
checked={!!checked}
data-testid={id}
id={id || uniqueId}
offText={formatMessage('Off')}
onChange={(_e, checked) => onToggle(!!checked)}
onText={formatMessage('On')}
/>
</div>
)}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ import wordWrap from './word-wrap.svg';
import autoUpdate from './auto-update.svg';
import earlyAdopters from './early-adopters.svg';
import language from './language.svg';
import previewFeatures from './preview-features.svg';

export { minimap, onboarding, lineNumbers, wordWrap, autoUpdate, earlyAdopters, language };
export { minimap, onboarding, lineNumbers, wordWrap, autoUpdate, earlyAdopters, language, previewFeatures };
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions Composer/packages/client/src/pages/setting/app-settings/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,16 @@ export const settingsDescription = css`
export const image = css`
width: 86px;
`;

export const featureFlagGroupContainer = css`
margin-left: 166px;
font-size: ${FontSizes.size12};
`;

export const featureFlagContainer = css`
margin-bottom: 15px;
`;

export const featureFlagTitle = css`
font-weight: ${FontWeights.semibold};
`;
7 changes: 6 additions & 1 deletion Composer/packages/client/src/recoilModel/atoms/appState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT License.

import { atom, atomFamily } from 'recoil';
import { ProjectTemplate, UserSettings } from '@bfc/shared';
import { FeatureFlagMap, ProjectTemplate, UserSettings } from '@bfc/shared';
import { ExtensionMetadata } from '@bfc/extension-client';

import {
Expand Down Expand Up @@ -101,6 +101,11 @@ export const userSettingsState = atom<UserSettings>({
default: getUserSettings(),
});

export const featureFlagsState = atom<FeatureFlagMap>({
key: getFullyQualifiedKey('featureFlag'),
default: {} as FeatureFlagMap,
});

export const announcementState = atom<string>({
key: getFullyQualifiedKey('announcement'),
default: '',
Expand Down
28 changes: 28 additions & 0 deletions Composer/packages/client/src/recoilModel/dispatchers/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { useRecoilCallback, CallbackInterface } from 'recoil';
import isArray from 'lodash/isArray';
import formatMessage from 'format-message';
import { FeatureFlagMap } from '@bfc/shared';

import httpClient from '../../utils/httpUtil';
import {
Expand All @@ -13,6 +14,7 @@ import {
applicationErrorState,
templateProjectsState,
runtimeTemplatesState,
featureFlagsState,
} from '../atoms/appState';
import { FileTypes } from '../../constants';
import { getExtension } from '../../utils/fileUtil';
Expand Down Expand Up @@ -167,6 +169,30 @@ export const storageDispatcher = () => {
}
);

const fetchFeatureFlags = useRecoilCallback<[], Promise<void>>((callbackHelpers: CallbackInterface) => async () => {
const { set } = callbackHelpers;
try {
const response = await httpClient.get('/featureFlags');
set(featureFlagsState, response.data);
} catch (ex) {
logMessage(callbackHelpers, `Error fetching feature flag data: ${ex}`);
}
});

const toggleFeatureFlag = useRecoilCallback(
({ set }: CallbackInterface) => async (featureName: string, enabled: boolean) => {
let newFeatureFlags: FeatureFlagMap = {} as FeatureFlagMap;
// update local
set(featureFlagsState, (featureFlagsState) => {
newFeatureFlags = { ...featureFlagsState };
newFeatureFlags[featureName] = { ...featureFlagsState[featureName], enabled: enabled };
return newFeatureFlags;
});
// update server
await httpClient.post(`/featureFlags`, { featureFlags: newFeatureFlags });
}
);

return {
fetchStorages,
updateCurrentPathForStorage,
Expand All @@ -178,5 +204,7 @@ export const storageDispatcher = () => {
updateFolder,
fetchTemplates,
fetchRuntimeTemplates,
fetchFeatureFlags,
toggleFeatureFlag,
};
};
34 changes: 34 additions & 0 deletions Composer/packages/lib/shared/src/featureFlagUtils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import formatMessage from 'format-message';

export type FeatureFlag = {
// Name to be displayed for this features toggle UI in app settings page
displayName: string;
// Description to be displayed for this features toggle UI in app settings page
description: string;
// Indicates whether or not the feature flag toggle will be visible to the user through the settings page UI
// Hidden feature flags are intended for features not ready for public preview
isHidden: boolean;
enabled: boolean;
};

export type FeatureFlagKey = 'VA_CREATION' | 'SHOW_FORM_DIALOG';

export type FeatureFlagMap = Record<FeatureFlagKey, FeatureFlag>;

export const defaultFeatureFlags: FeatureFlagMap = {
VA_CREATION: {
displayName: formatMessage('VA Creation'),
description: formatMessage('VA template made available in new bot flow.'),
isHidden: false,
enabled: false,
},
SHOW_FORM_DIALOG: {
displayName: formatMessage('Show Form Dialog'),
description: formatMessage('Show form dialog editor in the canvas'),
isHidden: true,
enabled: false,
},
};
1 change: 1 addition & 0 deletions Composer/packages/lib/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ export * from './viewUtils';
export * from './walkerUtils';
export * from './skillsUtils';
export * from './fileUtils';
export * from './featureFlagUtils';
export const DialogUtils = dialogUtils;
Loading