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 6 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
5 changes: 5 additions & 0 deletions Composer/packages/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@ initializeIcons(undefined, { disableWarnings: true });

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

useEffect(() => {
fetchFeatureFlags();
}, []);

const { fetchExtensions } = useRecoilValue(dispatcherState);
useEffect(() => {
fetchExtensions();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const CreationFlow: React.FC<CreationFlowProps> = () => {
createNewBot,
saveProjectAs,
fetchProjectById,
fetchFeatureFlags,
} = useRecoilValue(dispatcherState);

const creationFlowStatus = useRecoilValue(creationFlowStatusState);
Expand Down
78 changes: 41 additions & 37 deletions Composer/packages/client/src/pages/home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { RouteComponentProps } from '@reach/router';
import { navigate } from '@reach/router';
import { useRecoilValue } from 'recoil';
import { Feature } from '@bfc/shared';

import { CreationFlowStatus } from '../../constants';
import { dispatcherState, botDisplayNameState } from '../../recoilModel';
Expand All @@ -18,6 +19,7 @@ import {
templateProjectsState,
templateIdState,
currentProjectIdState,
featureFlagState,
} from '../../recoilModel/atoms/appState';
import { Toolbar, IToolbarItem } from '../../components/Toolbar';

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

return (
<div css={home.outline}>
<Toolbar toolbarItems={toolbarItems} />
Expand Down Expand Up @@ -195,45 +197,47 @@ const Home: React.FC<RouteComponentProps> = () => {
/>
</div>
)}
<div css={home.leftContainer}>
<h2 css={home.subtitle}>{formatMessage('Video tutorials:')}&nbsp;</h2>
<div css={home.newBotContainer}>
{tutorials.map((item, index) => (
<ItemContainer
key={index}
ariaLabel={item.title}
content={item.content}
href={item.href}
rel="noopener nofollow"
styles={home.tutorialTile}
subContent={item.subContent}
target="_blank"
title={item.title}
/>
))}
<div css={home.linkContainer}>
<div>
{formatMessage(
'Bot Framework provides the most comprehensive experience for building conversational applications.'
)}
<Feature featureFlagMap={featureFlagMap} featureFlagName="Show Tutorial">
<div css={home.leftContainer}>
<h2 css={home.subtitle}>{formatMessage('Video tutorials:')}&nbsp;</h2>
<div css={home.newBotContainer}>
{tutorials.map((item, index) => (
<ItemContainer
key={index}
ariaLabel={item.title}
content={item.content}
href={item.href}
rel="noopener nofollow"
styles={home.tutorialTile}
subContent={item.subContent}
target="_blank"
title={item.title}
/>
))}
<div css={home.linkContainer}>
<div>
{formatMessage(
'Bot Framework provides the most comprehensive experience for building conversational applications.'
)}
</div>
{linksButtom.map((link) => {
return (
<Link
key={'homePageLeftLinks-' + link.text}
href={link.to}
rel="noopener noreferrer"
style={{ width: '150px' }}
tabIndex={0}
target="_blank"
>
<div css={link.css}>{link.text}</div>
</Link>
);
})}
</div>
{linksButtom.map((link) => {
return (
<Link
key={'homePageLeftLinks-' + link.text}
href={link.to}
rel="noopener noreferrer"
style={{ width: '150px' }}
tabIndex={0}
target="_blank"
>
<div css={link.css}>{link.text}</div>
</Link>
);
})}
</div>
</div>
</div>
</Feature>
</div>
<div aria-label={formatMessage('Example bot list')} css={home.rightPage} role="region">
<h3 css={home.bluetitle}>{formatMessage(`Examples`)}</h3>
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 } from '@bfc/shared';

import { isElectron } from '../../../utils/electronUtil';
import { onboardingState, userSettingsState, dispatcherState } from '../../../recoilModel';
import { onboardingState, userSettingsState, dispatcherState, featureFlagState } 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 { FeatureFlagToggle } from './FeatureFlagToggle';

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, setFeatureFlag } = useRecoilValue(dispatcherState);
const userSettings = useRecoilValue(userSettingsState);
const { complete } = useRecoilValue(onboardingState);

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

const renderFeatureFlagOptions = () => {
const result: JSX.Element[] = [];
Object.keys(featureFlags).forEach((key: string) => {
const featureFlag: FeatureFlag = featureFlags[key];
if (!featureFlag.isHidden) {
result.push(
<FeatureFlagToggle
description={featureFlag.description}
featureFlagName={key}
setFeatureFlag={setFeatureFlag}
value={featureFlag.value}
/>
);
}
});
return <div css={featureFlagGroupContainer}>{result}</div>;
};

return (
<div css={container}>
<section css={section}>
Expand Down Expand Up @@ -155,6 +176,19 @@ const AppSettings: React.FC<RouteComponentProps> = () => {
onChange={onLocaleChange}
/>
</section>
<section css={section}>
<h2>{formatMessage('Application Updates')}</h2>
<SettingToggle
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);
}}
/>
{featureFlagVisible && renderFeatureFlagOptions()}
</section>
<Suspense fallback={<div />}>{renderElectronSettings && <ElectronSettings />}</Suspense>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/** @jsx jsx */
import { jsx } from '@emotion/core';
import React, { Fragment } from 'react';
import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox';
import { Stack } from 'office-ui-fabric-react/lib/Stack';
import { FeatureFlag, FeatureFlagNames } from '@bfc/shared';

import * as styles from './styles';

type FeatureFlagToggleProps = {
featureFlagName: string;
description: string;
value: boolean;
setFeatureFlag: (featureFlagName: string, value: boolean) => {};
};

const renderLabel = (featureName: string, description: string) => (
props: any,
defaultRender?: (props: any) => JSX.Element | null
) => {
return (
<span>
<span css={styles.featureFlagTitle}>{`${featureName}.`}</span>
{` ${description}`}
</span>
);
};

export const FeatureFlagToggle: React.FC<FeatureFlagToggleProps> = (props) => {
return (
<Checkbox
checked={props.value}
css={styles.featureFlagContainer}
onChange={(e: any, checked?: boolean) => {
if (checked !== undefined) {
props.setFeatureFlag(props.featureFlagName, checked);
}
}}
onRenderLabel={renderLabel(props.featureFlagName, props.description)}
/>
);
};
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 featureFlagState = atom<FeatureFlagMap>({
key: getFullyQualifiedKey('featureFlag'),
default: {} as FeatureFlagMap,
});

export const announcementState = atom<string>({
key: getFullyQualifiedKey('announcement'),
default: '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@

import { CallbackInterface, useRecoilCallback } from 'recoil';
import debounce from 'lodash/debounce';
import { FeatureFlagMap } from '@bfc/shared';

import { appUpdateState, announcementState, onboardingState, creationFlowStatusState } from '../atoms/appState';
import {
appUpdateState,
announcementState,
onboardingState,
creationFlowStatusState,
featureFlagState,
} from '../atoms/appState';
import { AppUpdaterStatus, CreationFlowStatus } from '../../constants';
import OnboardingState from '../../utils/onboardingStorage';
import { StateError, AppUpdateState } from '../../recoilModel/types';
import httpClient from '../../utils/httpUtil';

import { setError } from './shared';

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,
featureFlagState,
} 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/getFlags');
set(featureFlagState, response.data);
} catch (ex) {
logMessage(callbackHelpers, `Error fetching feature flag data: ${ex}`);
}
});

const setFeatureFlag = useRecoilCallback(
({ set }: CallbackInterface) => async (featureName: string, value: boolean) => {
let newFeatureFlagState: FeatureFlagMap = {};
// update local
set(featureFlagState, (featureFlagState) => {
newFeatureFlagState = { ...featureFlagState };
newFeatureFlagState[featureName] = { ...featureFlagState[featureName], value: value };
return newFeatureFlagState;
});
// update server
await httpClient.post(`/featureFlags/updateFlags`, { featureFlags: newFeatureFlagState });
}
);

return {
fetchStorages,
updateCurrentPathForStorage,
Expand All @@ -178,5 +204,7 @@ export const storageDispatcher = () => {
updateFolder,
fetchTemplates,
fetchRuntimeTemplates,
fetchFeatureFlags,
setFeatureFlag,
};
};
Loading