feat: frontend consumption of feature flags (#17926)

pull/17929/head
Alex Boatwright 2020-04-30 16:44:09 -07:00 committed by GitHub
parent 22899aee25
commit 7be7328c3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 372 additions and 230 deletions

View File

@ -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<WithRouterProps> = ({router}) => {
interface DispatchProps {
resetFeatureFlags: typeof reset
}
type Props = DispatchProps & WithRouterProps
const Logout: FC<Props> = ({router, resetFeatureFlags}) => {
const handleSignOut = async () => {
if (CLOUD) {
window.location.href = `${CLOUD_URL}${CLOUD_LOGOUT_PATH}`
@ -28,9 +35,19 @@ const Logout: FC<WithRouterProps> = ({router}) => {
}
useEffect(() => {
resetFeatureFlags()
handleSignOut()
}, [])
return null
}
export default ErrorHandling(withRouter<WithRouterProps>(Logout))
const mdtp = {
resetFeatureFlags: reset,
}
export default ErrorHandling(
connect<{}, DispatchProps>(
null,
mdtp
)(withRouter<WithRouterProps>(Logout))
)

View File

@ -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 {
</Route>
<Route component={Signin}>
<Route component={GetMe}>
<Route component={GetOrganizations}>
<Route path="/">
<Route path="no-orgs" component={NoOrgsPage} />
<IndexRoute component={RouteToOrg} />
<Route path="orgs" component={App}>
<Route path="new" component={CreateOrgOverlay} />
<Route path=":orgID" component={SetOrg}>
<IndexRoute component={MePage} />
<Route path="tasks" component={TasksPage}>
<Route
path=":id/export"
component={TaskExportOverlay}
/>
<Route
path="import"
component={TaskImportOverlay}
/>
<Route
path="import/template"
component={TaskImportFromTemplateOverlay}
/>
</Route>
<Route
path="tasks/:id/runs"
component={TaskRunsPage}
/>
<Route path="tasks/new" component={TaskPage} />
<Route path="tasks/:id" component={TaskEditPage} />
<Route
path="data-explorer"
component={DataExplorerPage}
>
<Route path="save" component={SaveAsOverlay} />
<Route
path="delete-data"
component={DEDeleteDataOverlay}
/>
</Route>
<Route path="dashboards" component={DashboardsIndex}>
<Route
path="import"
component={DashboardImportOverlay}
/>
<Route
path="import/template"
component={CreateFromTemplateOverlay}
/>
<Route
path=":dashboardID/export"
component={DashboardExportOverlay}
/>
</Route>
<Route
path="dashboards/:dashboardID"
component={DashboardContainer}
>
<Route path="cells">
<Route path="new" component={NewVEO} />
<Route path=":cellID/edit" component={EditVEO} />
</Route>
<Route path="notes">
<Route path="new" component={AddNoteOverlay} />
<Route component={GetFlags}>
<Route component={GetOrganizations}>
<Route path="/">
<Route path="no-orgs" component={NoOrgsPage} />
<IndexRoute component={RouteToOrg} />
<Route path="orgs" component={App}>
<Route path="new" component={CreateOrgOverlay} />
<Route path=":orgID" component={SetOrg}>
<IndexRoute component={MePage} />
<Route path="tasks" component={TasksPage}>
<Route
path=":cellID/edit"
component={EditNoteOverlay}
/>
</Route>
</Route>
<Route path="me" component={MePage} />
<Route path="load-data">
<IndexRoute component={BucketsIndex} />
<Route path="tokens" component={TokensIndex}>
<Route path="generate">
<Route
path="all-access"
component={AllAccessTokenOverlay}
/>
<Route
path="buckets"
component={BucketsTokenOverlay}
/>
</Route>
</Route>
<Route path="buckets" component={BucketsIndex}>
<Route path=":bucketID">
<Route
path="line-protocols/new"
component={LineProtocolWizard}
/>
<Route
path="telegrafs/new"
component={CollectorsWizard}
/>
<Route
path="scrapers/new"
component={CreateScraperOverlay}
/>
<Route
path="edit"
component={UpdateBucketOverlay}
/>
<Route
path="delete-data"
component={BucketsDeleteDataOverlay}
/>
<Route
path="rename"
component={RenameBucketOverlay}
/>
</Route>
</Route>
<Route path="telegrafs" component={TelegrafsPage}>
<Route
path=":id/view"
component={TelegrafConfigOverlay}
path=":id/export"
component={TaskExportOverlay}
/>
<Route
path=":id/instructions"
component={TelegrafInstructionsOverlay}
path="import"
component={TaskImportOverlay}
/>
<Route
path="output"
component={TelegrafOutputOverlay}
/>
<Route path="new" component={CollectorsWizard} />
</Route>
<Route path="scrapers" component={ScrapersIndex}>
<Route
path="new"
component={CreateScraperOverlay}
path="import/template"
component={TaskImportFromTemplateOverlay}
/>
</Route>
<Route
path="client-libraries"
component={ClientLibrariesPage}
path="tasks/:id/runs"
component={TaskRunsPage}
/>
<Route path="tasks/new" component={TaskPage} />
<Route path="tasks/:id" component={TaskEditPage} />
<Route
path="data-explorer"
component={DataExplorerPage}
>
<Route path="save" component={SaveAsOverlay} />
<Route
path="delete-data"
component={DEDeleteDataOverlay}
/>
</Route>
<Route
path="dashboards"
component={DashboardsIndex}
>
<Route
path="csharp"
component={ClientCSharpOverlay}
/>
<Route path="go" component={ClientGoOverlay} />
<Route
path="java"
component={ClientJavaOverlay}
/>
<Route
path="javascript-node"
component={ClientJSOverlay}
/>
<Route path="php" component={ClientPHPOverlay} />
<Route
path="python"
component={ClientPythonOverlay}
/>
<Route
path="ruby"
component={ClientRubyOverlay}
/>
</Route>
</Route>
<Route path="settings">
<IndexRoute component={VariablesIndex} />
<Route path="variables" component={VariablesIndex}>
<Route
path="import"
component={VariableImportOverlay}
component={DashboardImportOverlay}
/>
<Route
path=":id/export"
component={VariableExportOverlay}
path="import/template"
component={CreateFromTemplateOverlay}
/>
<Route
path="new"
component={CreateVariableOverlay}
/>
<Route
path=":id/rename"
component={RenameVariableOverlay}
/>
<Route
path=":id/edit"
component={UpdateVariableOverlay}
path=":dashboardID/export"
component={DashboardExportOverlay}
/>
</Route>
<Route path="templates" component={TemplatesIndex}>
<Route
path="dashboards/:dashboardID"
component={DashboardContainer}
>
<Route path="cells">
<Route path="new" component={NewVEO} />
<Route
path=":cellID/edit"
component={EditVEO}
/>
</Route>
<Route path="notes">
<Route path="new" component={AddNoteOverlay} />
<Route
path=":cellID/edit"
component={EditNoteOverlay}
/>
</Route>
</Route>
<Route path="me" component={MePage} />
<Route path="load-data">
<IndexRoute component={BucketsIndex} />
<Route path="tokens" component={TokensIndex}>
<Route path="generate">
<Route
path="all-access"
component={AllAccessTokenOverlay}
/>
<Route
path="buckets"
component={BucketsTokenOverlay}
/>
</Route>
</Route>
<Route path="buckets" component={BucketsIndex}>
<Route path=":bucketID">
<Route
path="line-protocols/new"
component={LineProtocolWizard}
/>
<Route
path="telegrafs/new"
component={CollectorsWizard}
/>
<Route
path="scrapers/new"
component={CreateScraperOverlay}
/>
<Route
path="edit"
component={UpdateBucketOverlay}
/>
<Route
path="delete-data"
component={BucketsDeleteDataOverlay}
/>
<Route
path="rename"
component={RenameBucketOverlay}
/>
</Route>
</Route>
<Route path="telegrafs" component={TelegrafsPage}>
<Route
path=":id/view"
component={TelegrafConfigOverlay}
/>
<Route
path=":id/instructions"
component={TelegrafInstructionsOverlay}
/>
<Route
path="output"
component={TelegrafOutputOverlay}
/>
<Route
path="new"
component={CollectorsWizard}
/>
</Route>
<Route path="scrapers" component={ScrapersIndex}>
<Route
path="new"
component={CreateScraperOverlay}
/>
</Route>
<Route
path="import"
component={TemplateImportOverlay}
path="client-libraries"
component={ClientLibrariesPage}
>
<Route
path="csharp"
component={ClientCSharpOverlay}
/>
<Route path="go" component={ClientGoOverlay} />
<Route
path="java"
component={ClientJavaOverlay}
/>
<Route
path="javascript-node"
component={ClientJSOverlay}
/>
<Route
path="php"
component={ClientPHPOverlay}
/>
<Route
path="python"
component={ClientPythonOverlay}
/>
<Route
path="ruby"
component={ClientRubyOverlay}
/>
</Route>
</Route>
<Route path="settings">
<IndexRoute component={VariablesIndex} />
<Route
path="variables"
component={VariablesIndex}
>
<Route
path="import"
component={VariableImportOverlay}
/>
<Route
path=":id/export"
component={VariableExportOverlay}
/>
<Route
path="new"
component={CreateVariableOverlay}
/>
<Route
path=":id/rename"
component={RenameVariableOverlay}
/>
<Route
path=":id/edit"
component={UpdateVariableOverlay}
/>
</Route>
<Route
path="templates"
component={TemplatesIndex}
>
<Route
path="import"
component={TemplateImportOverlay}
/>
<Route
path=":id/export"
component={TemplateExportOverlay}
/>
<Route
path=":id/view"
component={TemplateViewOverlay}
/>
<Route
path=":id/static/view"
component={StaticTemplateViewOverlay}
/>
</Route>
<Route path="labels" component={LabelsIndex} />
<Route path="about" component={OrgProfilePage}>
<Route
path="rename"
component={RenameOrgOverlay}
/>
</Route>
</Route>
<Route path="alerting" component={AlertingIndex}>
<Route
path="checks/new-threshold"
component={NewThresholdCheckEO}
/>
<Route
path=":id/export"
component={TemplateExportOverlay}
path="checks/new-deadman"
component={NewDeadmanCheckEO}
/>
<Route
path=":id/view"
component={TemplateViewOverlay}
path="checks/:checkID/edit"
component={EditCheckEO}
/>
<Route
path=":id/static/view"
component={StaticTemplateViewOverlay}
/>
</Route>
<Route path="labels" component={LabelsIndex} />
<Route path="about" component={OrgProfilePage}>
<Route
path="rename"
component={RenameOrgOverlay}
path="rules/new"
component={NewRuleOverlay}
/>
<Route
path="rules/:ruleID/edit"
component={EditRuleOverlay}
/>
<Route
path="endpoints/new"
component={NewEndpointOverlay}
/>
<Route
path="endpoints/:endpointID/edit"
component={EditEndpointOverlay}
/>
</Route>
<Route
path="alert-history"
component={AlertHistoryIndex}
/>
<Route
path="checks/:checkID"
component={CheckHistory}
/>
<Route path="about" component={OrgProfilePage} />
{!CLOUD && (
<Route path="members" component={MembersIndex} />
)}
</Route>
<Route path="alerting" component={AlertingIndex}>
<Route
path="checks/new-threshold"
component={NewThresholdCheckEO}
/>
<Route
path="checks/new-deadman"
component={NewDeadmanCheckEO}
/>
<Route
path="checks/:checkID/edit"
component={EditCheckEO}
/>
<Route
path="rules/new"
component={NewRuleOverlay}
/>
<Route
path="rules/:ruleID/edit"
component={EditRuleOverlay}
/>
<Route
path="endpoints/new"
component={NewEndpointOverlay}
/>
<Route
path="endpoints/:endpointID/edit"
component={EditEndpointOverlay}
/>
</Route>
<Route
path="alert-history"
component={AlertHistoryIndex}
/>
<Route
path="checks/:checkID"
component={CheckHistory}
/>
<Route path="about" component={OrgProfilePage} />
{!CLOUD && (
<Route path="members" component={MembersIndex} />
)}
</Route>
</Route>
</Route>

View File

@ -26,6 +26,7 @@ export const localState: LocalStorage = {
},
},
flags: {
status: RemoteDataState.Done,
original: {},
override: {},
},

View File

@ -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<typeof setFlags>
| ReturnType<typeof reset>
| ReturnType<typeof clearOverrides>
| ReturnType<typeof setOverride>
// 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<Actions>
): Promise<FlagMap> => {
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))
}
}

View File

@ -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<any>
}
interface DispatchProps {
getFlags: typeof getFlagsAction
}
interface StateProps {
status: RemoteDataState
}
type Props = StateProps & DispatchProps & PassedInProps
const GetFlags: FunctionComponent<Props> = ({status, getFlags, children}) => {
useEffect(() => {
if (status === RemoteDataState.NotStarted) {
getFlags()
}
}, [])
return (
<SpinnerContainer loading={status} spinnerComponent={<TechnoSpinner />}>
{children && React.cloneElement(children)}
</SpinnerContainer>
)
}
const mdtp = {
getFlags: getFlagsAction,
}
const mstp = (state: AppState): StateProps => ({
status: state.flags.status || RemoteDataState.NotStarted,
})
export default connect<StateProps, DispatchProps, PassedInProps>(
mstp,
mdtp
)(GetFlags)

View File

@ -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 {