feat(sampledata): Add demo data dropdown and membership request to buckets (#17454)

* feat(sampledata): Create Add demodata button behind feature flag

* feat(sampledata): Add demodata actions and reducers

* feat(sampledata): Add sampledata api functions

* feat(sampledata): Place bucket fetching behind feature flag

* feat(sampledata): Clean up

* feat(sampledata): Add actions to demodata dropdown

* feat(sampledata): No need to map over demodata buckets

* feat(sampledata: Fetch demo data buckets user is member to

* feat(sampledata): const is const

* feat(sampledata): Add demodata to testID string

* feat(sampledata): simplify buckets endpoint check

* feat(sampledata): Remove feature flag component for isFlagEnabled function
pull/17465/head
Deniz Kusefoglu 2020-03-26 17:31:47 -07:00 committed by GitHub
parent 844abce937
commit ad38ed1215
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 345 additions and 18 deletions

View File

@ -49,6 +49,7 @@ import {
removeBucketLabelFailed,
} from 'src/shared/copy/notifications'
import {LIMIT} from 'src/resources/constants'
import {getDemoDataBucketsFromAll} from 'src/cloud/apis/demodata'
type Action = BucketAction | NotifyAction
@ -71,8 +72,10 @@ export const getBuckets = () => async (
throw new Error(resp.data.message)
}
const demoDataBuckets = await getDemoDataBucketsFromAll()
const buckets = normalize<Bucket, BucketEntities, string[]>(
resp.data.buckets,
[...resp.data.buckets, ...demoDataBuckets],
arrayOfBuckets
)

View File

@ -53,4 +53,13 @@
.system-bucket {
color: mix($c-honeydew, $g13-mist);
}
}
.buckets-buttons-wrap {
display: flex;
box-sizing: border-box;
text-align: right;
@media screen and (max-width: 680px) {
margin: 8px auto 0;
}
}

View File

@ -1,6 +1,6 @@
// Libraries
import React, {PureComponent} from 'react'
import {isEmpty} from 'lodash'
import {isEmpty, get} from 'lodash'
import {connect} from 'react-redux'
// Components
@ -21,10 +21,10 @@ import SearchWidget from 'src/shared/components/search_widget/SearchWidget'
import SettingsTabbedPageHeader from 'src/settings/components/SettingsTabbedPageHeader'
import FilterList from 'src/shared/components/FilterList'
import BucketList from 'src/buckets/components/BucketList'
import {PrettyBucket} from 'src/buckets/components/BucketCard'
import CreateBucketOverlay from 'src/buckets/components/CreateBucketOverlay'
import AssetLimitAlert from 'src/cloud/components/AssetLimitAlert'
import BucketExplainer from 'src/buckets/components/BucketExplainer'
import DemoDataDropdown from 'src/buckets/components/DemoDataDropdown'
// Actions
import {
@ -36,14 +36,20 @@ import {
checkBucketLimits as checkBucketLimitsAction,
LimitStatus,
} from 'src/cloud/actions/limits'
import {
getDemoDataBuckets as getDemoDataBucketsAction,
getDemoDataBucketMembership as getDemoDataBucketMembershipAction,
} from 'src/cloud/actions/demodata'
// Utils
import {prettyBuckets} from 'src/shared/utils/prettyBucket'
import {extractBucketLimits} from 'src/cloud/utils/limits'
import {getOrg} from 'src/organizations/selectors'
import {getAll} from 'src/resources/selectors'
import {isFlagEnabled} from 'src/shared/utils/featureFlag'
// Types
import {PrettyBucket} from 'src/buckets/components/BucketCard'
import {
OverlayState,
AppState,
@ -57,6 +63,7 @@ interface StateProps {
org: Organization
buckets: Bucket[]
limitStatus: LimitStatus
demoDataBuckets: Bucket[]
}
interface DispatchProps {
@ -64,6 +71,8 @@ interface DispatchProps {
updateBucket: typeof updateBucket
deleteBucket: typeof deleteBucket
checkBucketLimits: typeof checkBucketLimitsAction
getDemoDataBuckets: typeof getDemoDataBucketsAction
getDemoDataBucketMembership: typeof getDemoDataBucketMembershipAction
}
interface State {
@ -96,10 +105,19 @@ class BucketsTab extends PureComponent<Props, State> {
public componentDidMount() {
this.props.checkBucketLimits()
if (isFlagEnabled('demodata')) {
this.props.getDemoDataBuckets()
}
}
public render() {
const {org, buckets, limitStatus} = this.props
const {
org,
buckets,
limitStatus,
demoDataBuckets,
getDemoDataBucketMembership,
} = this.props
const {
searchTerm,
overlayState,
@ -121,15 +139,23 @@ class BucketsTab extends PureComponent<Props, State> {
searchTerm={searchTerm}
onSearch={this.handleFilterChange}
/>
<Button
text="Create Bucket"
icon={IconFont.Plus}
color={ComponentColor.Primary}
onClick={this.handleOpenModal}
testID="Create Bucket"
status={this.createButtonStatus}
titleText={this.createButtonTitleText}
/>
<div className="buckets-buttons-wrap">
{isFlagEnabled('demodata') && demoDataBuckets.length > 0 && (
<DemoDataDropdown
buckets={demoDataBuckets}
getMembership={getDemoDataBucketMembership}
/>
)}
<Button
text="Create Bucket"
icon={IconFont.Plus}
color={ComponentColor.Primary}
onClick={this.handleOpenModal}
testID="Create Bucket"
status={this.createButtonStatus}
titleText={this.createButtonTitleText}
/>
</div>
</SettingsTabbedPageHeader>
<Grid>
<Grid.Row>
@ -259,13 +285,16 @@ const mstp = (state: AppState): StateProps => ({
org: getOrg(state),
buckets: getAll<Bucket>(state, ResourceType.Buckets),
limitStatus: extractBucketLimits(state.cloud.limits),
demoDataBuckets: get(state, 'cloud.demoData.buckets', []),
})
const mdtp = {
const mdtp: DispatchProps = {
createBucket,
updateBucket,
deleteBucket,
checkBucketLimits: checkBucketLimitsAction,
getDemoDataBuckets: getDemoDataBucketsAction,
getDemoDataBucketMembership: getDemoDataBucketMembershipAction,
}
export default connect<StateProps, DispatchProps, {}>(

View File

@ -0,0 +1,52 @@
// Libraries
import React, {FC} from 'react'
import _ from 'lodash'
// Components
import {IconFont, ComponentColor, Dropdown} from '@influxdata/clockface'
// Types
import {Bucket} from 'src/types'
import {getDemoDataBucketMembership as getDemoDataBucketMembershipAction} from 'src/cloud/actions/demodata'
interface Props {
buckets: Bucket[]
getMembership: typeof getDemoDataBucketMembershipAction
}
const DemoDataDropdown: FC<Props> = ({buckets, getMembership}) => {
const demoDataItems = buckets.map(b => (
<Dropdown.Item
testID={`dropdown-item--demodata-${b.name}`}
id={b.id}
key={b.id}
value={b.id}
onClick={getMembership}
>
{b.name}
</Dropdown.Item>
))
return (
<Dropdown
testID="dropdown--demodata"
style={{width: '160px', marginRight: '8px'}}
button={(active, onClick) => (
<Dropdown.Button
active={active}
onClick={onClick}
icon={IconFont.Plus}
color={ComponentColor.Secondary}
testID="dropdown-button--demodata"
>
Add Demo Data
</Dropdown.Button>
)}
menu={onCollapse => (
<Dropdown.Menu onCollapse={onCollapse}>{demoDataItems}</Dropdown.Menu>
)}
/>
)
}
export default DemoDataDropdown

View File

@ -0,0 +1,85 @@
// API
import {
getDemoDataBuckets as getDemoDataBucketsAJAX,
getDemoDataBucketMembership as getDemoDataBucketMembershipAJAX,
deleteDemoDataBucketMembership as deleteDemoDataBucketMembershipAJAX,
} from 'src/cloud/apis/demodata'
// Types
import {Bucket, RemoteDataState, GetState} from 'src/types'
import {getBuckets} from 'src/buckets/actions/thunks'
export type Actions =
| ReturnType<typeof setDemoDataStatus>
| ReturnType<typeof setDemoDataBuckets>
export const setDemoDataStatus = (status: RemoteDataState) => ({
type: 'SET_DEMODATA_STATUS' as 'SET_DEMODATA_STATUS',
payload: {status},
})
export const setDemoDataBuckets = (buckets: Bucket[]) => ({
type: 'SET_DEMODATA_BUCKETS' as 'SET_DEMODATA_BUCKETS',
payload: {buckets},
})
export const getDemoDataBuckets = () => async (
dispatch,
getState: GetState
) => {
const {
cloud: {
demoData: {status},
},
} = getState()
if (status === RemoteDataState.NotStarted) {
dispatch(setDemoDataStatus(RemoteDataState.Loading))
}
try {
const buckets = await getDemoDataBucketsAJAX()
dispatch(setDemoDataStatus(RemoteDataState.Done))
dispatch(setDemoDataBuckets(buckets))
} catch (error) {
console.error(error)
dispatch(setDemoDataStatus(RemoteDataState.Error))
}
}
export const getDemoDataBucketMembership = (bucketID: string) => async (
dispatch,
getState: GetState
) => {
const {
me: {id: userID},
} = getState()
try {
await getDemoDataBucketMembershipAJAX(bucketID, userID)
dispatch(getBuckets())
// TODO: check for success and error appropriately
// TODO: instantiate dashboard template
} catch (error) {
console.error(error)
}
}
export const deleteDemoDataBucketMembership = (bucketID: string) => async (
dispatch,
getState: GetState
) => {
const {
me: {id: userID},
} = getState()
try {
await deleteDemoDataBucketMembershipAJAX(bucketID, userID)
dispatch(getBuckets())
// TODO: check for success and error appropriately
// TODO: delete associated dashboard
} catch (error) {
console.error(error)
}
}

View File

@ -0,0 +1,93 @@
// Libraries
import {get} from 'lodash'
import * as api from 'src/client'
import AJAX from 'src/utils/ajax'
//Utils
import {isFlagEnabled} from 'src/shared/utils/featureFlag'
import {isDemoData} from 'src/cloud/utils/filterDemoData'
//Types
import {Bucket} from 'src/types'
import {LIMIT} from 'src/resources/constants'
const baseURL = '/api/v2/experimental/sampledata'
export const getDemoDataBuckets = async (): Promise<Bucket[]> => {
try {
const {data} = await AJAX({
method: 'GET',
url: `${baseURL}/buckets`,
})
// if sampledata endpoints are not available in a cluster
// gateway responds with a list of links where 'buckets' field is a string
const buckets = get(data, 'buckets', false)
if (!Array.isArray(buckets)) {
throw new Error('Could not reach demodata endpoint')
}
return buckets.filter(b => b.type == 'user') as Bucket[] // remove returned _tasks and _monitoring buckets
} catch (error) {
console.error(error)
throw error
}
}
export const getDemoDataBucketMembership = async (
bucketID: string,
userID: string
) => {
try {
const response = await AJAX({
method: 'POST',
url: `${baseURL}/buckets/${bucketID}/members`,
data: {userID},
})
if (response.status === '200') {
// a failed or successful membership POST to sampledata should return 204
throw new Error('Could not reach demodata endpoint')
}
} catch (error) {
console.error(error)
throw error
}
}
export const deleteDemoDataBucketMembership = async (
bucketID: string,
userID: string
) => {
try {
const response = await AJAX({
method: 'DELETE',
url: `${baseURL}/buckets/${bucketID}/members/${userID}`,
})
if (response.status === '200') {
// a failed or successful membership DELETE to sampledata should return 204
throw new Error('Could not reach demodata endpoint')
}
} catch (error) {
console.error(error)
throw error
}
}
export const getDemoDataBucketsFromAll = async (): Promise<Bucket[]> => {
if (!isFlagEnabled('demodata')) return []
try {
const resp = await api.getBuckets({query: {limit: LIMIT}})
if (resp.status !== 200) {
throw new Error(resp.data.message)
}
return resp.data.buckets
.filter(isDemoData)
.map(b => ({...b, type: 'system' as 'system', labels: []}))
} catch (error) {
console.error(error)
// demodata bucket fetching errors should not effect regular bucket fetching
}
}

View File

@ -0,0 +1,35 @@
//Types
import {Actions} from 'src/cloud/actions/demodata'
import {RemoteDataState, Bucket} from 'src/types'
export interface DemoDataState {
buckets: Bucket[]
status: RemoteDataState
}
export const defaultState: DemoDataState = {
buckets: [],
status: RemoteDataState.NotStarted,
}
export const demoDataReducer = (
state = defaultState,
action: Actions
): DemoDataState => {
switch (action.type) {
case 'SET_DEMODATA_STATUS': {
return {...state, status: action.payload.status}
}
case 'SET_DEMODATA_BUCKETS': {
return {
...state,
status: RemoteDataState.Done,
buckets: action.payload.buckets,
}
}
default:
return state
}
}
export default demoDataReducer

View File

@ -0,0 +1,9 @@
const DemoDataBucketNames = ['Website Monitoring Bucket']
export const isDemoData = (bucket): boolean => {
if (DemoDataBucketNames.includes(bucket.name)) {
//bucket is demo data bucket
return true
}
return false
}

View File

@ -9,6 +9,7 @@ export const OSS_FLAGS = {
matchingNotificationRules: false,
regionBasedLoginPage: false,
treeNav: false,
demodata: false,
}
export const CLOUD_FLAGS = {
@ -21,6 +22,7 @@ export const CLOUD_FLAGS = {
matchingNotificationRules: false,
regionBasedLoginPage: false,
treeNav: false,
demodata: false,
}
export const isFlagEnabled = (flagName: string, equals?: string | boolean) => {

View File

@ -35,6 +35,7 @@ import {userSettingsReducer} from 'src/userSettings/reducers'
import {membersReducer} from 'src/members/reducers'
import {autoRefreshReducer} from 'src/shared/reducers/autoRefresh'
import {limitsReducer, LimitsState} from 'src/cloud/reducers/limits'
import {demoDataReducer, DemoDataState} from 'src/cloud/reducers/demodata'
import checksReducer from 'src/checks/reducers'
import rulesReducer from 'src/notifications/rules/reducers'
import endpointsReducer from 'src/notifications/endpoints/reducers'
@ -56,7 +57,10 @@ export const rootReducer = combineReducers<ReducerState>({
...sharedReducers,
autoRefresh: autoRefreshReducer,
alertBuilder: alertBuilderReducer,
cloud: combineReducers<{limits: LimitsState}>({limits: limitsReducer}),
cloud: combineReducers<{limits: LimitsState; demoData: DemoDataState}>({
limits: limitsReducer,
demoData: demoDataReducer,
}),
currentPage: currentPageReducer,
currentDashboard: currentDashboardReducer,
dataLoading: dataLoadingReducer,

View File

@ -31,6 +31,7 @@ import {getAll} from 'src/resources/selectors'
// Constants
import {LIMIT} from 'src/resources/constants'
import {getDemoDataBucketsFromAll} from 'src/cloud/apis/demodata'
export type Action =
| ReturnType<typeof setBuilderAggregateFunctionType>
@ -160,7 +161,11 @@ export const loadBuckets = () => async (
throw new Error(resp.data.message)
}
const allBuckets = resp.data.buckets.map(b => b.name)
const demoDataBuckets = await getDemoDataBucketsFromAll()
const allBuckets = [...resp.data.buckets, ...demoDataBuckets].map(
b => b.name
)
const systemBuckets = allBuckets.filter(b => b.startsWith('_'))
const userBuckets = allBuckets.filter(b => !b.startsWith('_'))

View File

@ -23,6 +23,7 @@ import {AutoRefreshState} from 'src/shared/reducers/autoRefresh'
import {LimitsState} from 'src/cloud/reducers/limits'
import {AlertBuilderState} from 'src/alerting/reducers/alertBuilder'
import {CurrentPage} from 'src/shared/reducers/currentPage'
import {DemoDataState} from 'src/cloud/reducers/demodata'
import {ResourceState} from 'src/types'
@ -30,7 +31,7 @@ export interface AppState {
alertBuilder: AlertBuilderState
app: AppPresentationState
autoRefresh: AutoRefreshState
cloud: {limits: LimitsState}
cloud: {limits: LimitsState; demoData: DemoDataState}
currentPage: CurrentPage
currentDashboard: CurrentDashboardState
dataLoading: DataLoadingState