From 7be7328c3bd307f779c69c809b24d30808f72b12 Mon Sep 17 00:00:00 2001 From: Alex Boatwright Date: Thu, 30 Apr 2020 16:44:09 -0700 Subject: [PATCH] feat: frontend consumption of feature flags (#17926) --- ui/src/Logout.tsx | 21 +- ui/src/index.tsx | 471 ++++++++++++++------------ ui/src/mockState.tsx | 1 + ui/src/shared/actions/flags.ts | 38 ++- ui/src/shared/containers/GetFlags.tsx | 53 +++ ui/src/shared/reducers/flags.ts | 18 +- 6 files changed, 372 insertions(+), 230 deletions(-) create mode 100644 ui/src/shared/containers/GetFlags.tsx diff --git a/ui/src/Logout.tsx b/ui/src/Logout.tsx index 1f510d8d72..39b86e9a94 100644 --- a/ui/src/Logout.tsx +++ b/ui/src/Logout.tsx @@ -1,5 +1,6 @@ // Libraries import {FC, useEffect} from 'react' +import {connect} from 'react-redux' import {withRouter, WithRouterProps} from 'react-router' // APIs @@ -10,8 +11,14 @@ import {CLOUD, CLOUD_URL, CLOUD_LOGOUT_PATH} from 'src/shared/constants' // Components import {ErrorHandling} from 'src/shared/decorators/errors' +import {reset} from 'src/shared/actions/flags' -const Logout: FC = ({router}) => { +interface DispatchProps { + resetFeatureFlags: typeof reset +} + +type Props = DispatchProps & WithRouterProps +const Logout: FC = ({router, resetFeatureFlags}) => { const handleSignOut = async () => { if (CLOUD) { window.location.href = `${CLOUD_URL}${CLOUD_LOGOUT_PATH}` @@ -28,9 +35,19 @@ const Logout: FC = ({router}) => { } useEffect(() => { + resetFeatureFlags() handleSignOut() }, []) return null } -export default ErrorHandling(withRouter(Logout)) +const mdtp = { + resetFeatureFlags: reset, +} + +export default ErrorHandling( + connect<{}, DispatchProps>( + null, + mdtp + )(withRouter(Logout)) +) diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 92aca08e9c..cba9886f25 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -38,6 +38,7 @@ import {MePage} from 'src/me' import NotFound from 'src/shared/components/NotFound' import GetLinks from 'src/shared/containers/GetLinks' import GetMe from 'src/shared/containers/GetMe' +import GetFlags from 'src/shared/containers/GetFlags' import UnauthenticatedApp from 'src/shared/containers/UnauthenticatedApp' import TaskExportOverlay from 'src/tasks/components/TaskExportOverlay' import TaskImportOverlay from 'src/tasks/components/TaskImportOverlay' @@ -201,258 +202,278 @@ class Root extends PureComponent { - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + {!CLOUD && ( + + )} - - - - - - - - - - - - - {!CLOUD && ( - - )} diff --git a/ui/src/mockState.tsx b/ui/src/mockState.tsx index 673d449961..e952852417 100644 --- a/ui/src/mockState.tsx +++ b/ui/src/mockState.tsx @@ -26,6 +26,7 @@ export const localState: LocalStorage = { }, }, flags: { + status: RemoteDataState.Done, original: {}, override: {}, }, diff --git a/ui/src/shared/actions/flags.ts b/ui/src/shared/actions/flags.ts index 34c15f0ffc..ae4cb7c7bf 100644 --- a/ui/src/shared/actions/flags.ts +++ b/ui/src/shared/actions/flags.ts @@ -1,19 +1,33 @@ +import {Dispatch} from 'redux' +import {getFlags as getFlagsRequest} from 'src/client' +import {FlagMap} from 'src/shared/reducers/flags' +import {RemoteDataState} from 'src/types' export const SET_FEATURE_FLAGS = 'SET_FEATURE_FLAGS' +export const RESET_FEATURE_FLAGS = 'RESET_FEATURE_FLAGS' export const CLEAR_FEATURE_FLAG_OVERRIDES = 'CLEAR_FEATURE_FLAG_OVERRIDES' export const SET_FEATURE_FLAG_OVERRIDE = 'SET_FEATURE_FLAG_OVERRIDE' export type Actions = | ReturnType + | ReturnType | ReturnType | ReturnType // NOTE: this doesnt have a type as it will be determined // by the backend at a later time and keeping the format // open for transformations in a bit -export const setFlags = flags => +export const setFlags = (status: RemoteDataState, flags?: FlagMap) => ({ type: SET_FEATURE_FLAGS, - payload: flags, + payload: { + status, + flags, + }, + } as const) + +export const reset = () => + ({ + type: RESET_FEATURE_FLAGS, } as const) export const clearOverrides = () => @@ -28,3 +42,23 @@ export const setOverride = (flag: string, value: string | boolean) => [flag]: value, }, } as const) + +export const getFlags = () => async ( + dispatch: Dispatch +): Promise => { + try { + dispatch(setFlags(RemoteDataState.Loading)) + const resp = await getFlagsRequest({}) + + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + dispatch(setFlags(RemoteDataState.Done, resp.data)) + + return resp.data + } catch (error) { + console.error(error) + dispatch(setFlags(RemoteDataState.Error, null)) + } +} diff --git a/ui/src/shared/containers/GetFlags.tsx b/ui/src/shared/containers/GetFlags.tsx new file mode 100644 index 0000000000..4cc226b46b --- /dev/null +++ b/ui/src/shared/containers/GetFlags.tsx @@ -0,0 +1,53 @@ +// Libraries +import React, {useEffect, FunctionComponent} from 'react' +import {connect} from 'react-redux' + +// Components +import {SpinnerContainer, TechnoSpinner} from '@influxdata/clockface' + +// Types +import {RemoteDataState, AppState} from 'src/types' + +// Actions +import {getFlags as getFlagsAction} from 'src/shared/actions/flags' + +interface PassedInProps { + children: React.ReactElement +} + +interface DispatchProps { + getFlags: typeof getFlagsAction +} + +interface StateProps { + status: RemoteDataState +} + +type Props = StateProps & DispatchProps & PassedInProps + +const GetFlags: FunctionComponent = ({status, getFlags, children}) => { + useEffect(() => { + if (status === RemoteDataState.NotStarted) { + getFlags() + } + }, []) + + return ( + }> + {children && React.cloneElement(children)} + + ) +} + +const mdtp = { + getFlags: getFlagsAction, +} + +const mstp = (state: AppState): StateProps => ({ + status: state.flags.status || RemoteDataState.NotStarted, +}) + +export default connect( + mstp, + mdtp +)(GetFlags) diff --git a/ui/src/shared/reducers/flags.ts b/ui/src/shared/reducers/flags.ts index 6fce9525ad..74abae816a 100644 --- a/ui/src/shared/reducers/flags.ts +++ b/ui/src/shared/reducers/flags.ts @@ -1,20 +1,24 @@ import { Actions, SET_FEATURE_FLAGS, + RESET_FEATURE_FLAGS, CLEAR_FEATURE_FLAG_OVERRIDES, SET_FEATURE_FLAG_OVERRIDE, } from 'src/shared/actions/flags' +import {RemoteDataState} from 'src/types' export interface FlagMap { [key: string]: string | boolean } export interface FlagState { + status: RemoteDataState original: FlagMap override: FlagMap } const defaultState: FlagState = { + status: RemoteDataState.NotStarted, original: {}, override: {}, } @@ -22,9 +26,21 @@ const defaultState: FlagState = { export default (state = defaultState, action: Actions): FlagState => { switch (action.type) { case SET_FEATURE_FLAGS: + // just setting the loading state + if (!action.payload.flags) { + return { + ...state, + status: action.payload.status, + } + } return { ...state, - original: action.payload, + status: action.payload.status, + original: action.payload.flags, + } + case RESET_FEATURE_FLAGS: + return { + ...defaultState, } case CLEAR_FEATURE_FLAG_OVERRIDES: return {