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 functionpull/17465/head
parent
844abce937
commit
ad38ed1215
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -54,3 +54,12 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, {}>(
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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) => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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('_'))
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue