Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow for new flag source #17806

Merged
merged 6 commits into from
Apr 27, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
31 changes: 31 additions & 0 deletions ui/src/shared/actions/flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export const SET_FEATURE_FLAGS = 'SET_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 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 => {
return {
type: SET_FEATURE_FLAGS,
payload: flags,
} as const
}

export const clearOverrides = () =>
({
type: CLEAR_FEATURE_FLAG_OVERRIDES,
} as const)

export const setOverride = (flag: string, value: string | boolean) =>
({
type: SET_FEATURE_FLAG_OVERRIDE,
payload: {
[flag]: value,
},
} as const)
28 changes: 10 additions & 18 deletions ui/src/shared/actions/me.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,17 @@ import {CLOUD} from 'src/shared/constants'
import HoneyBadger from 'honeybadger-js'
import {fireUserDataReady} from 'src/shared/utils/analytics'

export enum ActionTypes {
SetMe = 'SET_ME',
}

export interface SetMe {
type: ActionTypes.SetMe
payload: {
me: MeState
}
}
export const SET_ME = 'SET_ME'

export type Actions = SetMe
export type Actions = ReturnType<typeof setMe>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

migrated to new reducer coding standard while i was in here


export const setMe = me => ({
type: ActionTypes.SetMe,
payload: {
me,
},
})
export const setMe = (me: MeState) =>
({
type: SET_ME,
payload: {
me,
},
} as const)

export const getMe = () => async dispatch => {
try {
Expand All @@ -36,7 +28,7 @@ export const getMe = () => async dispatch => {
user_id: user.id,
})

dispatch(setMe(user))
dispatch(setMe(user as MeState))
} catch (error) {
console.error(error)
}
Expand Down
46 changes: 46 additions & 0 deletions ui/src/shared/reducers/flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
Actions,
SET_FEATURE_FLAGS,
CLEAR_FEATURE_FLAG_OVERRIDES,
SET_FEATURE_FLAG_OVERRIDE,
} from 'src/shared/actions/flags'

export interface FlagMap {
[key: string]: string | boolean
}

export interface FlagState {
original: FlagMap
override: FlagMap
}

const defaultState: FlagState = {
original: {},
override: {},
}

export default (state = defaultState, action: Actions): FlagState => {
switch (action.type) {
case SET_FEATURE_FLAGS:
return {
...state,
original: action.payload,
}
case CLEAR_FEATURE_FLAG_OVERRIDES:
return {
...state,
override: {},
}
case SET_FEATURE_FLAG_OVERRIDE:
const override = {
...(state.override || {}),
...action.payload,
}
return {
...state,
override,
}
default:
return state
}
}
11 changes: 7 additions & 4 deletions ui/src/shared/reducers/me.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Actions, ActionTypes} from 'src/shared/actions/me'
import {Actions, SET_ME} from 'src/shared/actions/me'

export interface MeLinks {
self: string
Expand All @@ -11,7 +11,7 @@ export interface MeState {
links: MeLinks
}

const defaultState = {
const defaultState: MeState = {
id: '',
name: '',
links: {
Expand All @@ -22,8 +22,11 @@ const defaultState = {

export default (state = defaultState, action: Actions): MeState => {
switch (action.type) {
case ActionTypes.SetMe:
return action.payload.me
case SET_ME:
return {
...state,
...action.payload.me,
}
default:
return state
}
Expand Down
39 changes: 39 additions & 0 deletions ui/src/shared/selectors/flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {AppState} from 'src/types'
import {FlagMap} from 'src/shared/reducers/flags'
import {CLOUD, CLOUD_BILLING_VISIBLE} from 'src/shared/constants'

export const OSS_FLAGS = {
deleteWithPredicate: false,
downloadCellCSV: false,
telegrafEditor: false,
customCheckQuery: false,
matchingNotificationRules: false,
regionBasedLoginPage: false,
demodata: false,
fluxParser: false,
}

export const CLOUD_FLAGS = {
deleteWithPredicate: false,
multiUser: false,
cloudBilling: CLOUD_BILLING_VISIBLE, // should be visible in dev and acceptance, but not in cloud
downloadCellCSV: false,
telegrafEditor: false,
customCheckQuery: false,
matchingNotificationRules: false,
regionBasedLoginPage: false,
demodata: false,
fluxParser: false,
}

export const activeFlags = (state: AppState): FlagMap => {
const localState = CLOUD ? CLOUD_FLAGS : OSS_FLAGS
const networkState = state.flags.original || {}
const override = state.flags.override || {}

return {
...localState,
...networkState,
...override,
}
}
2 changes: 1 addition & 1 deletion ui/src/shared/utils/featureFlag.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {CLOUD_FLAGS, OSS_FLAGS} from 'src/shared/utils/featureFlag'
import {CLOUD_FLAGS, OSS_FLAGS} from 'src/shared/selectors/flags'

describe("getting the user's feature flags", () => {
beforeEach(() => {
Expand Down
109 changes: 17 additions & 92 deletions ui/src/shared/utils/featureFlag.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,26 @@
import {FunctionComponent} from 'react'
import {CLOUD, CLOUD_BILLING_VISIBLE} from 'src/shared/constants'
import {activeFlags} from 'src/shared/selectors/flags'
import {clearOverrides, setOverride} from 'src/shared/actions/flags'

export const OSS_FLAGS = {
deleteWithPredicate: false,
downloadCellCSV: false,
telegrafEditor: false,
customCheckQuery: false,
matchingNotificationRules: false,
regionBasedLoginPage: false,
demodata: false,
fluxParser: false,
}

export const CLOUD_FLAGS = {
deleteWithPredicate: false,
multiUser: false,
cloudBilling: CLOUD_BILLING_VISIBLE, // should be visible in dev and acceptance, but not in cloud
downloadCellCSV: false,
telegrafEditor: false,
customCheckQuery: false,
matchingNotificationRules: false,
regionBasedLoginPage: false,
demodata: false,
fluxParser: false,
}
import configureStore from 'src/store/configureStore'

export const isFlagEnabled = (flagName: string, equals?: string | boolean) => {
let localStorageFlags
let _equals = equals

try {
localStorageFlags = JSON.parse(window.localStorage.featureFlags)
} catch {
localStorageFlags = {}
}
const store = configureStore()
const flags = activeFlags(store.getState())

if (_equals === undefined) {
_equals = true
}

if (localStorageFlags.hasOwnProperty(flagName)) {
return localStorageFlags[flagName] === _equals
}

if (CLOUD) {
if (CLOUD_FLAGS.hasOwnProperty(flagName)) {
return CLOUD_FLAGS[flagName] === _equals
}

return false
}

if (OSS_FLAGS.hasOwnProperty(flagName)) {
return OSS_FLAGS[flagName] === _equals
if (flags.hasOwnProperty(flagName)) {
return flags[flagName] === _equals
}

return false
}

// type influx.toggleFeature('myFlag') to disable / enable any feature flag
// type influx.toggle('myFlag') to disable / enable any feature flag
export const FeatureFlag: FunctionComponent<{
name: string
equals?: string | boolean
Expand All @@ -70,16 +32,7 @@ export const FeatureFlag: FunctionComponent<{
return children as any
}

export const getUserFlags = function getUserFlags() {
const flagKeys = CLOUD ? Object.keys(CLOUD_FLAGS) : Object.keys(OSS_FLAGS)

const flags = {}
flagKeys.forEach(key => {
flags[key] = isFlagEnabled(key)
})

return flags
}
export const getUserFlags = () => activeFlags(configureStore().getState())

/* eslint-disable no-console */
const list = () => {
Expand All @@ -89,50 +42,22 @@ const list = () => {
/* eslint-enable no-console */

const reset = () => {
const featureFlags = JSON.parse(window.localStorage.featureFlags || '{}')

if (CLOUD) {
Object.keys(featureFlags).forEach(k => {
if (!CLOUD_FLAGS.hasOwnProperty(k)) {
delete featureFlags[k]
} else {
featureFlags[k] = CLOUD_FLAGS[k]
}
})
} else {
Object.keys(featureFlags).forEach(k => {
if (!OSS_FLAGS.hasOwnProperty(k)) {
delete featureFlags[k]
} else {
featureFlags[k] = OSS_FLAGS[k]
}
})
}

window.localStorage.featureFlags = JSON.stringify(featureFlags)
const store = configureStore()
store.dispatch(clearOverrides())
}

export const set = (flagName: string, value: string | boolean) => {
const featureFlags = JSON.parse(window.localStorage.featureFlags || '{}')

featureFlags[flagName] = value

window.localStorage.featureFlags = JSON.stringify(featureFlags)

return featureFlags[flagName]
const store = configureStore()
store.dispatch(setOverride(flagName, value))
}

export const toggleLocalStorageFlag = (flagName: string) => {
const featureFlags = JSON.parse(window.localStorage.featureFlags || '{}')

featureFlags[flagName] = !featureFlags[flagName]

window.localStorage.featureFlags = JSON.stringify(featureFlags)
export const toggle = (flagName: string) => {
const flags = getUserFlags()

return featureFlags[flagName]
set(flagName, !flags[flagName])
}

// Expose utility in dev tools console for convenience
const w: any = window

w.influx = {toggleFeature: toggleLocalStorageFlag, list, reset, set}
w.influx = {toggle, list, reset, set}
14 changes: 11 additions & 3 deletions ui/src/store/configureStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import persistStateEnhancer from './persistStateEnhancer'

// v2 reducers
import meReducer from 'src/shared/reducers/me'
import flagReducer from 'src/shared/reducers/flags'
import currentDashboardReducer from 'src/shared/reducers/currentDashboard'
import currentPageReducer from 'src/shared/reducers/currentPage'
import tasksReducer from 'src/tasks/reducers'
Expand Down Expand Up @@ -65,6 +66,7 @@ export const rootReducer = combineReducers<ReducerState>({
currentDashboard: currentDashboardReducer,
dataLoading: dataLoadingReducer,
me: meReducer,
flags: flagReducer,
noteEditor: noteEditorReducer,
onboarding: onboardingReducer,
overlays: overlaysReducer,
Expand Down Expand Up @@ -102,10 +104,15 @@ export const rootReducer = combineReducers<ReducerState>({
const composeEnhancers =
(window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose

let _store

export default function configureStore(
initialState: LocalStorage,
history: History
initialState?: LocalStorage,
history?: History
): Store<AppState & LocalStorage> {
if (_store) {
return _store
}
const routingMiddleware = routerMiddleware(history)
const createPersistentStore = composeEnhancers(
persistStateEnhancer(),
Expand All @@ -120,5 +127,6 @@ export default function configureStore(
// https://github.com/elgerlambert/redux-localstorage/issues/42
// createPersistentStore should ONLY take reducer and initialState
// any store enhancers must be added to the compose() function.
return createPersistentStore(rootReducer, initialState)
_store = createPersistentStore(rootReducer, initialState)
return _store
}
Loading