feat(demodata): Notify on demodata success and failure. (#17902)
* feat(demodata): Do not delete dashboard when delete demodata bucket * feat(demodata): Refactor demodata dropdown * feat(demodata): Fetch DD_Buckets in DD_Dropdown * feat(demodata): Refactor notifications Co-authored-by: alexpaxton <thealexpaxton@gmail.com> * feat(demodata): Style demo data dropdown * feat(demodata): Remove double console logging of error * feat(demodata): Error on success and failure * feat(demodata): Remove duplicate console.error * feat(demodata): fix lint errors * feat(demodata): Reintroduce Notification Style and add testID * feat(demodata): Add dd success notification and links to notifications Co-authored-by: alexpaxton <thealexpaxton@gmail.com> * feat(demodata): fix test * feat(demodata): Simplify getDemoDataBucketMembership function * feat(demodata): Remove unused notification copy * feat(demodata): Extend notification duration * feat(demodata): Return null instead of false * feat(demodata): Do not console.error if also notifying user * feat(demodata): Add todo item Co-authored-by: alexpaxton <thealexpaxton@gmail.com>pull/17915/head
parent
463c8bab1f
commit
080d77751b
|
@ -61,7 +61,7 @@ describe('The Query Builder', () => {
|
||||||
// wait for the notification since it's highly animated
|
// wait for the notification since it's highly animated
|
||||||
// we close the notification since it contains the name of the dashboard and interfers with cy.contains
|
// we close the notification since it contains the name of the dashboard and interfers with cy.contains
|
||||||
cy.wait(250)
|
cy.wait(250)
|
||||||
cy.get('.notification-close').click()
|
cy.get('.cf-notification--dismiss').click()
|
||||||
cy.wait(250)
|
cy.wait(250)
|
||||||
|
|
||||||
// force a click on the hidden dashboard nav item (cypress can't do the hover)
|
// force a click on the hidden dashboard nav item (cypress can't do the hover)
|
||||||
|
|
|
@ -33,16 +33,10 @@ import {
|
||||||
checkBucketLimits as checkBucketLimitsAction,
|
checkBucketLimits as checkBucketLimitsAction,
|
||||||
LimitStatus,
|
LimitStatus,
|
||||||
} from 'src/cloud/actions/limits'
|
} from 'src/cloud/actions/limits'
|
||||||
import {
|
|
||||||
getDemoDataBuckets as getDemoDataBucketsAction,
|
|
||||||
getDemoDataBucketMembership as getDemoDataBucketMembershipAction,
|
|
||||||
} from 'src/cloud/actions/demodata'
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import {getNewDemoBuckets} from 'src/cloud/selectors/demodata'
|
|
||||||
import {extractBucketLimits} from 'src/cloud/utils/limits'
|
import {extractBucketLimits} from 'src/cloud/utils/limits'
|
||||||
import {getAll} from 'src/resources/selectors'
|
import {getAll} from 'src/resources/selectors'
|
||||||
import {isFlagEnabled} from 'src/shared/utils/featureFlag'
|
|
||||||
import {SortTypes} from 'src/shared/utils/sort'
|
import {SortTypes} from 'src/shared/utils/sort'
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
@ -52,7 +46,6 @@ import {BucketSortKey} from 'src/shared/components/resource_sort_dropdown/genera
|
||||||
interface StateProps {
|
interface StateProps {
|
||||||
buckets: Bucket[]
|
buckets: Bucket[]
|
||||||
limitStatus: LimitStatus
|
limitStatus: LimitStatus
|
||||||
demoDataBuckets: Bucket[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DispatchProps {
|
interface DispatchProps {
|
||||||
|
@ -60,8 +53,6 @@ interface DispatchProps {
|
||||||
updateBucket: typeof updateBucket
|
updateBucket: typeof updateBucket
|
||||||
deleteBucket: typeof deleteBucket
|
deleteBucket: typeof deleteBucket
|
||||||
checkBucketLimits: typeof checkBucketLimitsAction
|
checkBucketLimits: typeof checkBucketLimitsAction
|
||||||
getDemoDataBuckets: typeof getDemoDataBucketsAction
|
|
||||||
getDemoDataBucketMembership: typeof getDemoDataBucketMembershipAction
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
@ -90,18 +81,10 @@ class BucketsTab extends PureComponent<Props, State> {
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
this.props.checkBucketLimits()
|
this.props.checkBucketLimits()
|
||||||
if (isFlagEnabled('demodata')) {
|
|
||||||
this.props.getDemoDataBuckets()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const {
|
const {buckets, limitStatus} = this.props
|
||||||
buckets,
|
|
||||||
limitStatus,
|
|
||||||
demoDataBuckets,
|
|
||||||
getDemoDataBucketMembership,
|
|
||||||
} = this.props
|
|
||||||
const {searchTerm, sortKey, sortDirection, sortType} = this.state
|
const {searchTerm, sortKey, sortDirection, sortType} = this.state
|
||||||
|
|
||||||
const leftHeaderItems = (
|
const leftHeaderItems = (
|
||||||
|
@ -125,12 +108,7 @@ class BucketsTab extends PureComponent<Props, State> {
|
||||||
const rightHeaderItems = (
|
const rightHeaderItems = (
|
||||||
<>
|
<>
|
||||||
<FeatureFlag name="demodata">
|
<FeatureFlag name="demodata">
|
||||||
{demoDataBuckets.length > 0 && (
|
<DemoDataDropdown />
|
||||||
<DemoDataDropdown
|
|
||||||
buckets={demoDataBuckets}
|
|
||||||
getMembership={getDemoDataBucketMembership}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</FeatureFlag>
|
</FeatureFlag>
|
||||||
<CreateBucketButton />
|
<CreateBucketButton />
|
||||||
</>
|
</>
|
||||||
|
@ -229,7 +207,6 @@ const mstp = (state: AppState): StateProps => {
|
||||||
return {
|
return {
|
||||||
buckets,
|
buckets,
|
||||||
limitStatus: extractBucketLimits(state.cloud.limits),
|
limitStatus: extractBucketLimits(state.cloud.limits),
|
||||||
demoDataBuckets: getNewDemoBuckets(state, buckets),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,8 +215,6 @@ const mdtp: DispatchProps = {
|
||||||
updateBucket,
|
updateBucket,
|
||||||
deleteBucket,
|
deleteBucket,
|
||||||
checkBucketLimits: checkBucketLimitsAction,
|
checkBucketLimits: checkBucketLimitsAction,
|
||||||
getDemoDataBuckets: getDemoDataBucketsAction,
|
|
||||||
getDemoDataBucketMembership: getDemoDataBucketMembershipAction,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect<StateProps, DispatchProps, {}>(
|
export default connect<StateProps, DispatchProps, {}>(
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
.demodata-dropdown--item-contents {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demodata-dropdown--item-icon {
|
||||||
|
margin-right: $cf-marg-b;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demodata-dropdown--item__added,
|
||||||
|
.demodata-dropdown--item__added:hover {
|
||||||
|
.demodata-dropdown--item-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor: default;
|
||||||
|
background: none !important;
|
||||||
|
color: $c-honeydew !important;
|
||||||
|
}
|
|
@ -1,31 +1,97 @@
|
||||||
// Libraries
|
// Libraries
|
||||||
import React, {FC} from 'react'
|
import React, {FC, useEffect} from 'react'
|
||||||
import _ from 'lodash'
|
import {connect} from 'react-redux'
|
||||||
|
import {get, sortBy} from 'lodash'
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
import {getAll} from 'src/resources/selectors'
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
import {
|
||||||
|
getDemoDataBucketMembership as getDemoDataBucketMembershipAction,
|
||||||
|
getDemoDataBuckets as getDemoDataBucketsAction,
|
||||||
|
} from 'src/cloud/actions/demodata'
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import {IconFont, ComponentColor, Dropdown} from '@influxdata/clockface'
|
import {ComponentColor, Dropdown, Icon, IconFont} from '@influxdata/clockface'
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import {Bucket} from 'src/types'
|
import {AppState, Bucket, ResourceType} from 'src/types'
|
||||||
import {getDemoDataBucketMembership} from 'src/cloud/actions/demodata'
|
|
||||||
|
|
||||||
interface Props {
|
interface StateProps {
|
||||||
buckets: Bucket[]
|
ownBuckets: Bucket[]
|
||||||
getMembership: typeof getDemoDataBucketMembership
|
demoDataBuckets: Bucket[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const DemoDataDropdown: FC<Props> = ({buckets, getMembership}) => {
|
interface DispatchProps {
|
||||||
const demoDataItems = buckets.map(b => (
|
getDemoDataBucketMembership: typeof getDemoDataBucketMembershipAction
|
||||||
<Dropdown.Item
|
getDemoDataBuckets: typeof getDemoDataBucketsAction
|
||||||
testID={`dropdown-item--demodata-${b.name}`}
|
}
|
||||||
id={b.id}
|
|
||||||
key={b.id}
|
type Props = DispatchProps & StateProps
|
||||||
value={b}
|
|
||||||
onClick={getMembership}
|
const DemoDataDropdown: FC<Props> = ({
|
||||||
>
|
ownBuckets,
|
||||||
{b.name}
|
demoDataBuckets,
|
||||||
</Dropdown.Item>
|
getDemoDataBucketMembership,
|
||||||
))
|
getDemoDataBuckets,
|
||||||
|
}) => {
|
||||||
|
useEffect(() => {
|
||||||
|
getDemoDataBuckets()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!demoDataBuckets.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const ownBucketNames = ownBuckets.map(o => o.name.toLocaleLowerCase())
|
||||||
|
|
||||||
|
const sortedBuckets = sortBy(demoDataBuckets, d => {
|
||||||
|
return d.name.toLocaleLowerCase()
|
||||||
|
})
|
||||||
|
|
||||||
|
const dropdownItems = sortedBuckets.map(b => {
|
||||||
|
if (ownBucketNames.includes(b.name.toLocaleLowerCase())) {
|
||||||
|
return (
|
||||||
|
<Dropdown.Item
|
||||||
|
testID={`dropdown-item--demodata-${b.name}`}
|
||||||
|
className="demodata-dropdown--item__added"
|
||||||
|
id={b.id}
|
||||||
|
key={b.id}
|
||||||
|
value={b}
|
||||||
|
selected={true}
|
||||||
|
>
|
||||||
|
<div className="demodata-dropdown--item-contents">
|
||||||
|
<Icon
|
||||||
|
glyph={IconFont.Checkmark}
|
||||||
|
className="demodata-dropdown--item-icon"
|
||||||
|
/>
|
||||||
|
{b.name}
|
||||||
|
</div>
|
||||||
|
</Dropdown.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown.Item
|
||||||
|
testID={`dropdown-item--demodata-${b.name}`}
|
||||||
|
className="demodata-dropdown--item"
|
||||||
|
id={b.id}
|
||||||
|
key={b.id}
|
||||||
|
value={b}
|
||||||
|
onClick={getDemoDataBucketMembership}
|
||||||
|
selected={false}
|
||||||
|
>
|
||||||
|
<div className="demodata-dropdown--item-contents">
|
||||||
|
<Icon
|
||||||
|
glyph={IconFont.Checkmark}
|
||||||
|
className="demodata-dropdown--item-icon"
|
||||||
|
/>
|
||||||
|
{b.name}
|
||||||
|
</div>
|
||||||
|
</Dropdown.Item>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
@ -43,10 +109,23 @@ const DemoDataDropdown: FC<Props> = ({buckets, getMembership}) => {
|
||||||
</Dropdown.Button>
|
</Dropdown.Button>
|
||||||
)}
|
)}
|
||||||
menu={onCollapse => (
|
menu={onCollapse => (
|
||||||
<Dropdown.Menu onCollapse={onCollapse}>{demoDataItems}</Dropdown.Menu>
|
<Dropdown.Menu onCollapse={onCollapse}>{dropdownItems}</Dropdown.Menu>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DemoDataDropdown
|
const mstp = (state: AppState): StateProps => ({
|
||||||
|
ownBuckets: getAll<Bucket>(state, ResourceType.Buckets),
|
||||||
|
demoDataBuckets: get(state, 'cloud.demoData.buckets', []) as Bucket[],
|
||||||
|
})
|
||||||
|
|
||||||
|
const mdtp: DispatchProps = {
|
||||||
|
getDemoDataBucketMembership: getDemoDataBucketMembershipAction,
|
||||||
|
getDemoDataBuckets: getDemoDataBucketsAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect<StateProps, DispatchProps, {}>(
|
||||||
|
mstp,
|
||||||
|
mdtp
|
||||||
|
)(DemoDataDropdown)
|
||||||
|
|
|
@ -3,21 +3,26 @@ import {
|
||||||
getDemoDataBuckets as getDemoDataBucketsAJAX,
|
getDemoDataBuckets as getDemoDataBucketsAJAX,
|
||||||
getDemoDataBucketMembership as getDemoDataBucketMembershipAJAX,
|
getDemoDataBucketMembership as getDemoDataBucketMembershipAJAX,
|
||||||
deleteDemoDataBucketMembership as deleteDemoDataBucketMembershipAJAX,
|
deleteDemoDataBucketMembership as deleteDemoDataBucketMembershipAJAX,
|
||||||
|
getNormalizedDemoDataBucket,
|
||||||
} from 'src/cloud/apis/demodata'
|
} from 'src/cloud/apis/demodata'
|
||||||
import {createDashboardFromTemplate} from 'src/templates/api'
|
import {createDashboardFromTemplate} from 'src/templates/api'
|
||||||
import {deleteDashboard, getBucket} from 'src/client'
|
import {getBucket} from 'src/client'
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
import {getDashboards} from 'src/dashboards/actions/thunks'
|
|
||||||
import {addBucket, removeBucket} from 'src/buckets/actions/creators'
|
import {addBucket, removeBucket} from 'src/buckets/actions/creators'
|
||||||
|
import {notify} from 'src/shared/actions/notifications'
|
||||||
|
|
||||||
// Selectors
|
// Selectors
|
||||||
import {getOrg} from 'src/organizations/selectors'
|
import {getOrg} from 'src/organizations/selectors'
|
||||||
import {getAll} from 'src/resources/selectors/getAll'
|
import {getAll} from 'src/resources/selectors'
|
||||||
import {normalize} from 'normalizr'
|
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
import {DemoDataTemplates, DemoDataDashboards} from 'src/cloud/constants'
|
import {DemoDataTemplates, DemoDataDashboards} from 'src/cloud/constants'
|
||||||
|
import {
|
||||||
|
demoDataAddBucketFailed,
|
||||||
|
demoDataDeleteBucketFailed,
|
||||||
|
demoDataSucceeded,
|
||||||
|
} from 'src/shared/copy/notifications'
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import {
|
import {
|
||||||
|
@ -25,11 +30,10 @@ import {
|
||||||
RemoteDataState,
|
RemoteDataState,
|
||||||
GetState,
|
GetState,
|
||||||
DemoBucket,
|
DemoBucket,
|
||||||
Dashboard,
|
|
||||||
ResourceType,
|
ResourceType,
|
||||||
BucketEntities,
|
Dashboard,
|
||||||
} from 'src/types'
|
} from 'src/types'
|
||||||
import {bucketSchema} from 'src/schemas'
|
import {reportError} from 'src/shared/utils/errors'
|
||||||
|
|
||||||
export type Actions =
|
export type Actions =
|
||||||
| ReturnType<typeof setDemoDataStatus>
|
| ReturnType<typeof setDemoDataStatus>
|
||||||
|
@ -57,88 +61,74 @@ export const getDemoDataBuckets = () => async (
|
||||||
if (status === RemoteDataState.NotStarted) {
|
if (status === RemoteDataState.NotStarted) {
|
||||||
dispatch(setDemoDataStatus(RemoteDataState.Loading))
|
dispatch(setDemoDataStatus(RemoteDataState.Loading))
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const buckets = await getDemoDataBucketsAJAX()
|
const buckets = await getDemoDataBucketsAJAX()
|
||||||
|
|
||||||
dispatch(setDemoDataStatus(RemoteDataState.Done))
|
|
||||||
dispatch(setDemoDataBuckets(buckets))
|
dispatch(setDemoDataBuckets(buckets))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|
||||||
|
reportError(error, {
|
||||||
|
name: 'getDemoDataBuckets function',
|
||||||
|
})
|
||||||
|
|
||||||
dispatch(setDemoDataStatus(RemoteDataState.Error))
|
dispatch(setDemoDataStatus(RemoteDataState.Error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getDemoDataBucketMembership = (bucket: DemoBucket) => async (
|
export const getDemoDataBucketMembership = ({
|
||||||
dispatch,
|
name: bucketName,
|
||||||
getState: GetState
|
id: bucketID,
|
||||||
) => {
|
}) => async (dispatch, getState: GetState) => {
|
||||||
const state = getState()
|
const state = getState()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
me: {id: userID},
|
me: {id: userID},
|
||||||
} = state
|
} = state
|
||||||
|
|
||||||
const {id: orgID} = getOrg(state)
|
const {id: orgID} = getOrg(state)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await getDemoDataBucketMembershipAJAX(bucket.id, userID)
|
await getDemoDataBucketMembershipAJAX(bucketID, userID)
|
||||||
|
|
||||||
const template = await DemoDataTemplates[bucket.name]
|
const normalizedBucket = await getNormalizedDemoDataBucket(bucketID)
|
||||||
if (template) {
|
dispatch(addBucket(normalizedBucket))
|
||||||
await createDashboardFromTemplate(template, orgID)
|
|
||||||
} else {
|
const template = await DemoDataTemplates[bucketName]
|
||||||
|
if (!template) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Could not find template for demodata bucket ${bucket.name}`
|
`Could not find dashboard template for demodata bucket ${bucketName}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const resp = await getBucket({bucketID: bucket.id})
|
await createDashboardFromTemplate(template, orgID)
|
||||||
|
|
||||||
if (resp.status !== 200) {
|
|
||||||
throw new Error('Request for demo data bucket membership did not succeed')
|
|
||||||
}
|
|
||||||
|
|
||||||
const newBucket = {
|
|
||||||
...resp.data,
|
|
||||||
type: 'demodata' as 'demodata',
|
|
||||||
labels: [],
|
|
||||||
} as DemoBucket
|
|
||||||
|
|
||||||
const normalizedBucket = normalize<Bucket, BucketEntities, string>(
|
|
||||||
newBucket,
|
|
||||||
bucketSchema
|
|
||||||
)
|
|
||||||
|
|
||||||
dispatch(addBucket(normalizedBucket))
|
|
||||||
|
|
||||||
// TODO: notify success and error appropriately
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export const deleteDemoDataDashboard = (dashboardName: string) => async (
|
|
||||||
dispatch,
|
|
||||||
getState: GetState
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
await dispatch(getDashboards())
|
|
||||||
|
|
||||||
const updatedState = getState()
|
const updatedState = getState()
|
||||||
|
|
||||||
const ddDashboard = getAll(updatedState, ResourceType.Dashboards).find(
|
const allDashboards = getAll<Dashboard>(
|
||||||
d => {
|
updatedState,
|
||||||
d.name === dashboardName
|
ResourceType.Dashboards
|
||||||
}
|
)
|
||||||
) as Dashboard
|
|
||||||
|
|
||||||
if (ddDashboard) {
|
const createdDashboard = allDashboards.find(
|
||||||
const deleteResp = await deleteDashboard({
|
d => d.name === DemoDataDashboards[bucketName]
|
||||||
dashboardID: ddDashboard.id,
|
)
|
||||||
})
|
|
||||||
if (deleteResp.status !== 204) {
|
if (!createdDashboard) {
|
||||||
throw new Error(deleteResp.data.message)
|
throw new Error(
|
||||||
}
|
`Could not create dashboard for demodata bucket ${bucketName}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const url = `/orgs/${orgID}/dashboards/${createdDashboard.id}`
|
||||||
|
|
||||||
|
dispatch(notify(demoDataSucceeded(bucketName, url)))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(error)
|
dispatch(notify(demoDataAddBucketFailed(error)))
|
||||||
|
|
||||||
|
reportError(error, {
|
||||||
|
name: 'getDemoDataBucketMembership function',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,18 +150,11 @@ export const deleteDemoDataBucketMembership = (bucket: DemoBucket) => async (
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(removeBucket(bucket.id))
|
dispatch(removeBucket(bucket.id))
|
||||||
|
|
||||||
const demoDashboardName = DemoDataDashboards[bucket.name]
|
|
||||||
|
|
||||||
if (!demoDashboardName) {
|
|
||||||
throw new Error(
|
|
||||||
`Could not find dashboard name for demo data bucket ${bucket.name}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(deleteDemoDataDashboard(demoDashboardName))
|
|
||||||
// TODO: notify for success and error appropriately
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
dispatch(notify(demoDataDeleteBucketFailed(bucket.name, error)))
|
||||||
|
|
||||||
|
reportError(error, {
|
||||||
|
name: 'deleteDemoDataBucketMembership function',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,56 +1,50 @@
|
||||||
// Libraries
|
// Libraries
|
||||||
import {get} from 'lodash'
|
import {get} from 'lodash'
|
||||||
import {getBuckets} from 'src/client'
|
import {getBuckets, getBucket} from 'src/client'
|
||||||
import AJAX from 'src/utils/ajax'
|
import AJAX from 'src/utils/ajax'
|
||||||
|
|
||||||
//Utils
|
//Utils
|
||||||
import {isFlagEnabled} from 'src/shared/utils/featureFlag'
|
import {isFlagEnabled} from 'src/shared/utils/featureFlag'
|
||||||
|
|
||||||
//Types
|
//Types
|
||||||
import {Bucket, DemoBucket} from 'src/types'
|
import {Bucket, DemoBucket, BucketEntities} from 'src/types'
|
||||||
import {LIMIT} from 'src/resources/constants'
|
import {LIMIT} from 'src/resources/constants'
|
||||||
|
import {normalize} from 'normalizr'
|
||||||
|
import {bucketSchema} from 'src/schemas'
|
||||||
|
import {NormalizedSchema} from 'normalizr'
|
||||||
|
|
||||||
const baseURL = '/api/v2/experimental/sampledata'
|
const baseURL = '/api/v2/experimental/sampledata'
|
||||||
|
|
||||||
export const getDemoDataBuckets = async (): Promise<Bucket[]> => {
|
export const getDemoDataBuckets = async (): Promise<Bucket[]> => {
|
||||||
try {
|
//todo (deniz) convert to fetch
|
||||||
const {data} = await AJAX({
|
const {data} = await AJAX({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: `${baseURL}/buckets`,
|
url: `${baseURL}/buckets`,
|
||||||
})
|
})
|
||||||
|
|
||||||
// if sampledata endpoints are not available in a cluster
|
// if sampledata endpoints are not available in a cluster
|
||||||
// gateway responds with a list of links where 'buckets' field is a string
|
// gateway responds with a list of links where 'buckets' field is a string
|
||||||
const buckets = get(data, 'buckets', false)
|
const buckets = get(data, 'buckets', null)
|
||||||
if (!Array.isArray(buckets)) {
|
if (!Array.isArray(buckets)) {
|
||||||
throw new Error('Could not reach demodata endpoint')
|
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return buckets.filter(b => b.type == 'user') as Bucket[] // remove returned _tasks and _monitoring buckets
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getDemoDataBucketMembership = async (
|
export const getDemoDataBucketMembership = async (
|
||||||
bucketID: string,
|
bucketID: string,
|
||||||
userID: string
|
userID: string
|
||||||
) => {
|
) => {
|
||||||
try {
|
const response = await AJAX({
|
||||||
const response = await AJAX({
|
method: 'POST',
|
||||||
method: 'POST',
|
url: `${baseURL}/buckets/${bucketID}/members`,
|
||||||
url: `${baseURL}/buckets/${bucketID}/members`,
|
data: {userID},
|
||||||
data: {userID},
|
})
|
||||||
})
|
|
||||||
|
|
||||||
if (response.status === '200') {
|
if (response.status === '200') {
|
||||||
// a failed or successful membership POST to sampledata should return 204
|
// a failed or successful membership POST to sampledata should return 204
|
||||||
throw new Error('Could not reach demodata endpoint')
|
throw new Error('Could not reach demodata endpoint')
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,3 +100,30 @@ export const fetchDemoDataBuckets = async (): Promise<Bucket[]> => {
|
||||||
return [] // demodata bucket fetching errors should not effect regular bucket fetching
|
return [] // demodata bucket fetching errors should not effect regular bucket fetching
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getNormalizedDemoDataBucket = async (
|
||||||
|
bucketID: string
|
||||||
|
): Promise<NormalizedSchema<BucketEntities, string>> => {
|
||||||
|
const resp = await getBucket({bucketID})
|
||||||
|
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
throw new Error(
|
||||||
|
`Request for demo data bucket membership did not succeed: ${
|
||||||
|
resp.data.message
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const newBucket = {
|
||||||
|
...resp.data,
|
||||||
|
type: 'demodata' as 'demodata',
|
||||||
|
labels: [],
|
||||||
|
} as DemoBucket
|
||||||
|
|
||||||
|
const normalizedBucket = normalize<Bucket, BucketEntities, string>(
|
||||||
|
newBucket,
|
||||||
|
bucketSchema
|
||||||
|
)
|
||||||
|
|
||||||
|
return normalizedBucket
|
||||||
|
}
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
import {get, differenceBy, sortBy} from 'lodash'
|
|
||||||
import {AppState, Bucket, DemoBucket} from 'src/types'
|
|
||||||
|
|
||||||
export const getNewDemoBuckets = (state: AppState, ownBuckets: Bucket[]) => {
|
|
||||||
const demoDataBuckets = get(
|
|
||||||
state,
|
|
||||||
'cloud.demoData.buckets',
|
|
||||||
[]
|
|
||||||
) as DemoBucket[]
|
|
||||||
|
|
||||||
const newDemoDataBuckets = differenceBy(
|
|
||||||
demoDataBuckets,
|
|
||||||
ownBuckets,
|
|
||||||
b => b.id
|
|
||||||
)
|
|
||||||
|
|
||||||
return sortBy(newDemoDataBuckets, d => {
|
|
||||||
return d.name.toLocaleLowerCase()
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -104,7 +104,7 @@ export class OnboardingWizardPage extends PureComponent<Props, State> {
|
||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
spinnerComponent={<TechnoSpinner />}
|
spinnerComponent={<TechnoSpinner />}
|
||||||
>
|
>
|
||||||
<Notifications inPresentationMode={true} />
|
<Notifications />
|
||||||
<OnboardingWizard
|
<OnboardingWizard
|
||||||
onDecrementCurrentStepIndex={this.handleDecrementStepIndex}
|
onDecrementCurrentStepIndex={this.handleDecrementStepIndex}
|
||||||
onIncrementCurrentStepIndex={this.handleIncrementStepIndex}
|
onIncrementCurrentStepIndex={this.handleIncrementStepIndex}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
.notification--button {
|
||||||
|
display: inline-block;
|
||||||
|
margin: $cf-marg-a;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification--message {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: $cf-marg-a;
|
||||||
|
}
|
|
@ -1,137 +0,0 @@
|
||||||
import React, {Component, CSSProperties} from 'react'
|
|
||||||
import {connect} from 'react-redux'
|
|
||||||
import {bindActionCreators} from 'redux'
|
|
||||||
import {Notification as NotificationType} from 'src/types/notifications'
|
|
||||||
|
|
||||||
import classnames from 'classnames'
|
|
||||||
|
|
||||||
import {dismissNotification as dismissNotificationAction} from 'src/shared/actions/notifications'
|
|
||||||
|
|
||||||
import {NOTIFICATION_TRANSITION} from 'src/shared/constants/index'
|
|
||||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
notification: NotificationType
|
|
||||||
dismissNotification: (id: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
opacity: number
|
|
||||||
height: number
|
|
||||||
dismissed: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
@ErrorHandling
|
|
||||||
class Notification extends Component<Props, State> {
|
|
||||||
private notificationRef: HTMLElement
|
|
||||||
private dismissalTimer: number
|
|
||||||
private deletionTimer: number
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
opacity: 1,
|
|
||||||
height: 0,
|
|
||||||
dismissed: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidMount() {
|
|
||||||
const {
|
|
||||||
notification: {duration},
|
|
||||||
} = this.props
|
|
||||||
|
|
||||||
this.updateHeight()
|
|
||||||
|
|
||||||
if (duration >= 0) {
|
|
||||||
// Automatically dismiss notification after duration prop
|
|
||||||
this.dismissalTimer = window.setTimeout(this.handleDismiss, duration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentWillUnmount() {
|
|
||||||
clearTimeout(this.dismissalTimer)
|
|
||||||
clearTimeout(this.deletionTimer)
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
const {
|
|
||||||
notification: {message, icon},
|
|
||||||
} = this.props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={this.containerClassname} style={this.notificationStyle}>
|
|
||||||
<div
|
|
||||||
className={this.notificationClassname}
|
|
||||||
ref={this.handleNotificationRef}
|
|
||||||
data-testid={this.dataTestID}
|
|
||||||
>
|
|
||||||
<span className={`icon ${icon}`} />
|
|
||||||
<div className="notification-message">{message}</div>
|
|
||||||
<button className="notification-close" onClick={this.handleDismiss} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private get dataTestID(): string {
|
|
||||||
const {style} = this.props.notification
|
|
||||||
return `notification-${style}`
|
|
||||||
}
|
|
||||||
|
|
||||||
private get notificationClassname(): string {
|
|
||||||
const {
|
|
||||||
notification: {style},
|
|
||||||
} = this.props
|
|
||||||
|
|
||||||
return `notification notification-${style}`
|
|
||||||
}
|
|
||||||
|
|
||||||
private get containerClassname(): string {
|
|
||||||
const {height, dismissed} = this.state
|
|
||||||
|
|
||||||
return classnames('notification-container', {
|
|
||||||
show: !!height,
|
|
||||||
'notification-dismissed': dismissed,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private get notificationStyle(): CSSProperties {
|
|
||||||
return {height: '100%'}
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateHeight = (): void => {
|
|
||||||
if (this.notificationRef) {
|
|
||||||
const {height} = this.notificationRef.getBoundingClientRect()
|
|
||||||
this.setState({height})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleDismiss = (): void => {
|
|
||||||
const {
|
|
||||||
notification: {id},
|
|
||||||
dismissNotification,
|
|
||||||
} = this.props
|
|
||||||
|
|
||||||
this.setState({dismissed: true})
|
|
||||||
this.deletionTimer = window.setTimeout(
|
|
||||||
() => dismissNotification(id),
|
|
||||||
NOTIFICATION_TRANSITION
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleNotificationRef = (ref: HTMLElement): void => {
|
|
||||||
this.notificationRef = ref
|
|
||||||
this.updateHeight()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
dismissNotification: bindActionCreators(dismissNotificationAction, dispatch),
|
|
||||||
})
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
null,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(Notification)
|
|
|
@ -1,196 +0,0 @@
|
||||||
/*
|
|
||||||
Notifications
|
|
||||||
-----------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
$notification-margin: 12px;
|
|
||||||
|
|
||||||
.notification-center {
|
|
||||||
position: fixed;
|
|
||||||
right: $notification-margin;
|
|
||||||
width: 360px;
|
|
||||||
top: $chronograf-page-header-height + $notification-margin;
|
|
||||||
z-index: 9999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-center__presentation-mode {
|
|
||||||
@extend .notification-center;
|
|
||||||
top: $notification-margin;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification {
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 0;
|
|
||||||
border-radius: $ix-radius;
|
|
||||||
position: relative;
|
|
||||||
padding: 12px 40px;
|
|
||||||
@extend %no-user-select;
|
|
||||||
transform: translateX(105%);
|
|
||||||
transition: transform 0.25s ease 0.25s, opacity 0.25s ease;
|
|
||||||
|
|
||||||
> span.icon {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 20px;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
font-size: $ix-text-base-2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-message {
|
|
||||||
&:first-letter {
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-close {
|
|
||||||
outline: none;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
border: 0;
|
|
||||||
background-color: transparent;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
right: ($ix-marg-c - $ix-marg-a);
|
|
||||||
font-size: $ix-text-base;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
opacity: 0.25;
|
|
||||||
transition: opacity 0.25s ease;
|
|
||||||
|
|
||||||
&:before,
|
|
||||||
&:after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
width: 16px;
|
|
||||||
height: 2px;
|
|
||||||
border-radius: 1px;
|
|
||||||
background-color: $g20-white;
|
|
||||||
}
|
|
||||||
&:before {
|
|
||||||
transform: translate(-50%, -50%) rotate(-45deg);
|
|
||||||
}
|
|
||||||
&:after {
|
|
||||||
transform: translate(-50%, -50%) rotate(45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.notification-container {
|
|
||||||
overflow: hidden;
|
|
||||||
height: 0;
|
|
||||||
margin-bottom: $ix-marg-a;
|
|
||||||
transition: height 0.25s ease;
|
|
||||||
|
|
||||||
&.show .notification {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
&.notification-dismissed {
|
|
||||||
height: 0 !important;
|
|
||||||
.notification {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mixin for Alert Themes
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
@mixin notification-styles(
|
|
||||||
$bg-color,
|
|
||||||
$bg-color-2,
|
|
||||||
$text-color,
|
|
||||||
$link-color,
|
|
||||||
$link-hover
|
|
||||||
) {
|
|
||||||
font-size: 16px;
|
|
||||||
|
|
||||||
@include gradient-h($bg-color, $bg-color-2);
|
|
||||||
color: $text-color;
|
|
||||||
|
|
||||||
a:link,
|
|
||||||
a:visited {
|
|
||||||
color: $link-color;
|
|
||||||
font-weight: 700;
|
|
||||||
text-decoration: underline;
|
|
||||||
transition: color 0.25s ease;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: $link-hover;
|
|
||||||
border-color: $link-hover;
|
|
||||||
}
|
|
||||||
span.icon {
|
|
||||||
color: $text-color;
|
|
||||||
}
|
|
||||||
.notification-close:before,
|
|
||||||
.notification-close:after {
|
|
||||||
background-color: $text-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alert Themes
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
.notification-success {
|
|
||||||
@include notification-styles(
|
|
||||||
$c-rainforest,
|
|
||||||
$c-pool,
|
|
||||||
$g20-white,
|
|
||||||
$c-wasabi,
|
|
||||||
$g20-white
|
|
||||||
);
|
|
||||||
}
|
|
||||||
.notification-primary {
|
|
||||||
@include notification-styles(
|
|
||||||
$c-pool,
|
|
||||||
$c-ocean,
|
|
||||||
$g20-white,
|
|
||||||
$c-neutrino,
|
|
||||||
$g20-white
|
|
||||||
);
|
|
||||||
}
|
|
||||||
.notification-warning {
|
|
||||||
@include notification-styles(
|
|
||||||
$c-star,
|
|
||||||
$c-pool,
|
|
||||||
$g20-white,
|
|
||||||
$c-neutrino,
|
|
||||||
$g20-white
|
|
||||||
);
|
|
||||||
}
|
|
||||||
.notification-error {
|
|
||||||
@include notification-styles(
|
|
||||||
$c-curacao,
|
|
||||||
$c-star,
|
|
||||||
$g20-white,
|
|
||||||
$c-marmelade,
|
|
||||||
$g20-white
|
|
||||||
);
|
|
||||||
}
|
|
||||||
.notification-info {
|
|
||||||
@include notification-styles(
|
|
||||||
$g20-white,
|
|
||||||
$g16-pearl,
|
|
||||||
$g8-storm,
|
|
||||||
$ix-link-default,
|
|
||||||
$ix-link-default-hover
|
|
||||||
);
|
|
||||||
}
|
|
||||||
.notification-dark {
|
|
||||||
@include notification-styles(
|
|
||||||
$c-sapphire,
|
|
||||||
$c-shadow,
|
|
||||||
$c-moonstone,
|
|
||||||
$ix-link-default,
|
|
||||||
$ix-link-default-hover
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.endpoint-description--textarea {
|
|
||||||
max-height: 150;
|
|
||||||
}
|
|
|
@ -1,16 +1,42 @@
|
||||||
import React, {PureComponent} from 'react'
|
import React, {PureComponent} from 'react'
|
||||||
|
import {Link} from 'react-router'
|
||||||
import {connect} from 'react-redux'
|
import {connect} from 'react-redux'
|
||||||
import {Notification as NotificationType} from 'src/types/notifications'
|
import {get} from 'lodash'
|
||||||
import Notification from 'src/shared/components/notifications/Notification'
|
|
||||||
|
|
||||||
interface Props {
|
//Actions
|
||||||
|
import {dismissNotification as dismissNotificationAction} from 'src/shared/actions/notifications'
|
||||||
|
|
||||||
|
import {Notification, ComponentSize, Gradients} from '@influxdata/clockface'
|
||||||
|
|
||||||
|
//Types
|
||||||
|
import {
|
||||||
|
Notification as NotificationType,
|
||||||
|
NotificationStyle,
|
||||||
|
} from 'src/types/notifications'
|
||||||
|
|
||||||
|
interface StateProps {
|
||||||
notifications: NotificationType[]
|
notifications: NotificationType[]
|
||||||
inPresentationMode: boolean
|
}
|
||||||
|
|
||||||
|
interface DispatchProps {
|
||||||
|
dismissNotification: typeof dismissNotificationAction
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = StateProps & DispatchProps
|
||||||
|
|
||||||
|
const matchGradientToColor = (style: NotificationStyle): Gradients => {
|
||||||
|
const converter = {
|
||||||
|
[NotificationStyle.Primary]: Gradients.Primary,
|
||||||
|
[NotificationStyle.Warning]: Gradients.WarningLight,
|
||||||
|
[NotificationStyle.Success]: Gradients.HotelBreakfast,
|
||||||
|
[NotificationStyle.Error]: Gradients.DangerDark,
|
||||||
|
[NotificationStyle.Info]: Gradients.DefaultLight,
|
||||||
|
}
|
||||||
|
return get(converter, style, Gradients.DefaultLight)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Notifications extends PureComponent<Props> {
|
class Notifications extends PureComponent<Props> {
|
||||||
public static defaultProps = {
|
public static defaultProps = {
|
||||||
inPresentationMode: false,
|
|
||||||
notifications: [],
|
notifications: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,36 +44,56 @@ class Notifications extends PureComponent<Props> {
|
||||||
const {notifications} = this.props
|
const {notifications} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={this.className}>
|
<>
|
||||||
{notifications.map(n => (
|
{notifications.map(
|
||||||
<Notification key={n.id} notification={n} />
|
({id, style, icon, duration, message, link, linkText}) => {
|
||||||
))}
|
const gradient = matchGradientToColor(style)
|
||||||
</div>
|
|
||||||
|
let button
|
||||||
|
|
||||||
|
if (link && linkText) {
|
||||||
|
button = (
|
||||||
|
<Link
|
||||||
|
to={link}
|
||||||
|
className="notification--button cf-button cf-button-xs cf-button-default"
|
||||||
|
>
|
||||||
|
{linkText}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Notification
|
||||||
|
key={id}
|
||||||
|
id={id}
|
||||||
|
icon={icon}
|
||||||
|
duration={duration}
|
||||||
|
size={ComponentSize.ExtraSmall}
|
||||||
|
gradient={gradient}
|
||||||
|
onTimeout={this.props.dismissNotification}
|
||||||
|
onDismiss={this.props.dismissNotification}
|
||||||
|
testID={`notification-${style}`}
|
||||||
|
>
|
||||||
|
<span className="notification--message">{message}</span>
|
||||||
|
{button}
|
||||||
|
</Notification>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private get className(): string {
|
|
||||||
const {inPresentationMode} = this.props
|
|
||||||
|
|
||||||
if (inPresentationMode) {
|
|
||||||
return 'notification-center__presentation-mode'
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'notification-center'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = ({
|
const mapStateToProps = ({notifications}): StateProps => ({
|
||||||
notifications,
|
notifications,
|
||||||
app: {
|
|
||||||
ephemeral: {inPresentationMode},
|
|
||||||
},
|
|
||||||
}): Props => ({
|
|
||||||
notifications,
|
|
||||||
inPresentationMode,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const mdtp: DispatchProps = {
|
||||||
|
dismissNotification: dismissNotificationAction,
|
||||||
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
null
|
mdtp
|
||||||
)(Notifications)
|
)(Notifications)
|
||||||
|
|
|
@ -36,7 +36,7 @@ export const DASHBOARD_LAYOUT_ROW_HEIGHT = 83.5
|
||||||
export const NOTIFICATION_TRANSITION = 250
|
export const NOTIFICATION_TRANSITION = 250
|
||||||
export const FIVE_SECONDS = 5000
|
export const FIVE_SECONDS = 5000
|
||||||
export const TEN_SECONDS = 10000
|
export const TEN_SECONDS = 10000
|
||||||
export const INFINITE = -1
|
export const FIFTEEN_SECONDS = 15000
|
||||||
|
|
||||||
export const HOMEPAGE_PATHNAME = 'me'
|
export const HOMEPAGE_PATHNAME = 'me'
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,17 @@
|
||||||
import {binaryPrefixFormatter} from '@influxdata/giraffe'
|
import {binaryPrefixFormatter} from '@influxdata/giraffe'
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import {Notification} from 'src/types'
|
import {Notification, NotificationStyle} from 'src/types'
|
||||||
import {NotificationStyle} from 'src/types/notifications'
|
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
import {FIVE_SECONDS, TEN_SECONDS, INFINITE} from 'src/shared/constants/index'
|
import {
|
||||||
|
FIVE_SECONDS,
|
||||||
|
TEN_SECONDS,
|
||||||
|
FIFTEEN_SECONDS,
|
||||||
|
} from 'src/shared/constants/index'
|
||||||
import {QUICKSTART_SCRAPER_TARGET_URL} from 'src/dataLoaders/constants/pluginConfigs'
|
import {QUICKSTART_SCRAPER_TARGET_URL} from 'src/dataLoaders/constants/pluginConfigs'
|
||||||
import {QUICKSTART_DASHBOARD_NAME} from 'src/onboarding/constants/index'
|
import {QUICKSTART_DASHBOARD_NAME} from 'src/onboarding/constants/index'
|
||||||
|
import {IconFont} from '@influxdata/clockface'
|
||||||
|
|
||||||
const bytesFormatter = binaryPrefixFormatter({
|
const bytesFormatter = binaryPrefixFormatter({
|
||||||
suffix: 'B',
|
suffix: 'B',
|
||||||
|
@ -23,19 +27,19 @@ type NotificationExcludingMessage = Pick<
|
||||||
|
|
||||||
const defaultErrorNotification: NotificationExcludingMessage = {
|
const defaultErrorNotification: NotificationExcludingMessage = {
|
||||||
style: NotificationStyle.Error,
|
style: NotificationStyle.Error,
|
||||||
icon: 'alert-triangle',
|
icon: IconFont.AlertTriangle,
|
||||||
duration: TEN_SECONDS,
|
duration: TEN_SECONDS,
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultSuccessNotification: NotificationExcludingMessage = {
|
const defaultSuccessNotification: NotificationExcludingMessage = {
|
||||||
style: NotificationStyle.Success,
|
style: NotificationStyle.Success,
|
||||||
icon: 'checkmark',
|
icon: IconFont.Checkmark,
|
||||||
duration: FIVE_SECONDS,
|
duration: FIVE_SECONDS,
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultDeletionNotification: NotificationExcludingMessage = {
|
const defaultDeletionNotification: NotificationExcludingMessage = {
|
||||||
style: NotificationStyle.Primary,
|
style: NotificationStyle.Primary,
|
||||||
icon: 'trash',
|
icon: IconFont.Trash,
|
||||||
duration: FIVE_SECONDS,
|
duration: FIVE_SECONDS,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,8 +48,7 @@ const defaultDeletionNotification: NotificationExcludingMessage = {
|
||||||
|
|
||||||
export const newVersion = (version: string): Notification => ({
|
export const newVersion = (version: string): Notification => ({
|
||||||
style: NotificationStyle.Info,
|
style: NotificationStyle.Info,
|
||||||
icon: 'cubo-uniform',
|
icon: IconFont.Cubouniform,
|
||||||
duration: INFINITE,
|
|
||||||
message: `Welcome to the latest Chronograf${version}. Local settings cleared.`,
|
message: `Welcome to the latest Chronograf${version}. Local settings cleared.`,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -56,21 +59,20 @@ export const loadLocalSettingsFailed = (error: string): Notification => ({
|
||||||
|
|
||||||
export const presentationMode = (): Notification => ({
|
export const presentationMode = (): Notification => ({
|
||||||
style: NotificationStyle.Primary,
|
style: NotificationStyle.Primary,
|
||||||
icon: 'expand-b',
|
icon: IconFont.ExpandB,
|
||||||
duration: 7500,
|
duration: 7500,
|
||||||
message: 'Press ESC to exit Presentation Mode.',
|
message: 'Press ESC to exit Presentation Mode.',
|
||||||
})
|
})
|
||||||
|
|
||||||
export const sessionTimedOut = (): Notification => ({
|
export const sessionTimedOut = (): Notification => ({
|
||||||
style: NotificationStyle.Primary,
|
style: NotificationStyle.Primary,
|
||||||
icon: 'triangle',
|
icon: IconFont.Triangle,
|
||||||
duration: INFINITE,
|
|
||||||
message: 'Your session has timed out. Log in again to continue.',
|
message: 'Your session has timed out. Log in again to continue.',
|
||||||
})
|
})
|
||||||
|
|
||||||
export const resultTooLarge = (bytesRead: number): Notification => ({
|
export const resultTooLarge = (bytesRead: number): Notification => ({
|
||||||
style: NotificationStyle.Error,
|
style: NotificationStyle.Error,
|
||||||
icon: 'triangle',
|
icon: IconFont.Triangle,
|
||||||
duration: FIVE_SECONDS,
|
duration: FIVE_SECONDS,
|
||||||
message: `Large response truncated to first ${bytesFormatter(bytesRead)}`,
|
message: `Large response truncated to first ${bytesFormatter(bytesRead)}`,
|
||||||
})
|
})
|
||||||
|
@ -145,19 +147,19 @@ export const dashboardGetFailed = (
|
||||||
error: string
|
error: string
|
||||||
): Notification => ({
|
): Notification => ({
|
||||||
...defaultErrorNotification,
|
...defaultErrorNotification,
|
||||||
icon: 'dash-h',
|
icon: IconFont.DashH,
|
||||||
message: `Failed to load dashboard with id "${dashboardID}": ${error}`,
|
message: `Failed to load dashboard with id "${dashboardID}": ${error}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const dashboardUpdateFailed = (): Notification => ({
|
export const dashboardUpdateFailed = (): Notification => ({
|
||||||
...defaultErrorNotification,
|
...defaultErrorNotification,
|
||||||
icon: 'dash-h',
|
icon: IconFont.DashH,
|
||||||
message: 'Could not update dashboard',
|
message: 'Could not update dashboard',
|
||||||
})
|
})
|
||||||
|
|
||||||
export const dashboardDeleted = (name: string): Notification => ({
|
export const dashboardDeleted = (name: string): Notification => ({
|
||||||
...defaultSuccessNotification,
|
...defaultSuccessNotification,
|
||||||
icon: 'dash-h',
|
icon: IconFont.DashH,
|
||||||
message: `Dashboard ${name} deleted successfully.`,
|
message: `Dashboard ${name} deleted successfully.`,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -194,7 +196,7 @@ export const cellAdded = (
|
||||||
dashboardName?: string
|
dashboardName?: string
|
||||||
): Notification => ({
|
): Notification => ({
|
||||||
...defaultSuccessNotification,
|
...defaultSuccessNotification,
|
||||||
icon: 'dash-h',
|
icon: IconFont.DashH,
|
||||||
message: `Added new cell ${cellName + ' '}to dashboard ${dashboardName}`,
|
message: `Added new cell ${cellName + ' '}to dashboard ${dashboardName}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -217,7 +219,7 @@ export const cellUpdateFailed = (): Notification => ({
|
||||||
|
|
||||||
export const cellDeleted = (): Notification => ({
|
export const cellDeleted = (): Notification => ({
|
||||||
...defaultDeletionNotification,
|
...defaultDeletionNotification,
|
||||||
icon: 'dash-h',
|
icon: IconFont.DashH,
|
||||||
duration: 1900,
|
duration: 1900,
|
||||||
message: `Cell deleted from dashboard.`,
|
message: `Cell deleted from dashboard.`,
|
||||||
})
|
})
|
||||||
|
@ -235,7 +237,7 @@ export const removedDashboardLabelFailed = (): Notification => ({
|
||||||
// Variables & URL Queries
|
// Variables & URL Queries
|
||||||
export const invalidTimeRangeValueInURLQuery = (): Notification => ({
|
export const invalidTimeRangeValueInURLQuery = (): Notification => ({
|
||||||
...defaultErrorNotification,
|
...defaultErrorNotification,
|
||||||
icon: 'cube',
|
icon: IconFont.Cube,
|
||||||
message: `Invalid URL query value supplied for lower or upper time range.`,
|
message: `Invalid URL query value supplied for lower or upper time range.`,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -251,37 +253,37 @@ export const getVariableFailed = (): Notification => ({
|
||||||
|
|
||||||
export const createVariableFailed = (error: string): Notification => ({
|
export const createVariableFailed = (error: string): Notification => ({
|
||||||
...defaultErrorNotification,
|
...defaultErrorNotification,
|
||||||
icon: 'cube',
|
icon: IconFont.Cube,
|
||||||
message: `Failed to create variable: ${error}`,
|
message: `Failed to create variable: ${error}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const createVariableSuccess = (name: string): Notification => ({
|
export const createVariableSuccess = (name: string): Notification => ({
|
||||||
...defaultSuccessNotification,
|
...defaultSuccessNotification,
|
||||||
icon: 'cube',
|
icon: IconFont.Cube,
|
||||||
message: `Successfully created new variable: ${name}.`,
|
message: `Successfully created new variable: ${name}.`,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const deleteVariableFailed = (error: string): Notification => ({
|
export const deleteVariableFailed = (error: string): Notification => ({
|
||||||
...defaultErrorNotification,
|
...defaultErrorNotification,
|
||||||
icon: 'cube',
|
icon: IconFont.Cube,
|
||||||
message: `Failed to delete variable: ${error}`,
|
message: `Failed to delete variable: ${error}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const deleteVariableSuccess = (): Notification => ({
|
export const deleteVariableSuccess = (): Notification => ({
|
||||||
...defaultSuccessNotification,
|
...defaultSuccessNotification,
|
||||||
icon: 'cube',
|
icon: IconFont.Cube,
|
||||||
message: 'Successfully deleted the variable',
|
message: 'Successfully deleted the variable',
|
||||||
})
|
})
|
||||||
|
|
||||||
export const updateVariableFailed = (error: string): Notification => ({
|
export const updateVariableFailed = (error: string): Notification => ({
|
||||||
...defaultErrorNotification,
|
...defaultErrorNotification,
|
||||||
icon: 'cube',
|
icon: IconFont.Cube,
|
||||||
message: `Failed to update variable: ${error}`,
|
message: `Failed to update variable: ${error}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const updateVariableSuccess = (name: string): Notification => ({
|
export const updateVariableSuccess = (name: string): Notification => ({
|
||||||
...defaultSuccessNotification,
|
...defaultSuccessNotification,
|
||||||
icon: 'cube',
|
icon: IconFont.Cube,
|
||||||
message: `Successfully updated variable: ${name}.`,
|
message: `Successfully updated variable: ${name}.`,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -290,7 +292,7 @@ export const copyToClipboardSuccess = (
|
||||||
title: string = ''
|
title: string = ''
|
||||||
): Notification => ({
|
): Notification => ({
|
||||||
...defaultSuccessNotification,
|
...defaultSuccessNotification,
|
||||||
icon: 'dash-h',
|
icon: IconFont.Cube,
|
||||||
type: 'copyToClipboardSuccess',
|
type: 'copyToClipboardSuccess',
|
||||||
message: `${title} '${text}' has been copied to clipboard.`,
|
message: `${title} '${text}' has been copied to clipboard.`,
|
||||||
})
|
})
|
||||||
|
@ -448,6 +450,32 @@ export const getBucketFailed = (
|
||||||
message: `Failed to fetch bucket with id ${bucketID}: ${error}`,
|
message: `Failed to fetch bucket with id ${bucketID}: ${error}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Demodata buckets
|
||||||
|
|
||||||
|
export const demoDataAddBucketFailed = (error: string): Notification => ({
|
||||||
|
...defaultErrorNotification,
|
||||||
|
message: error,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const demoDataDeleteBucketFailed = (
|
||||||
|
bucketName: string,
|
||||||
|
error: string
|
||||||
|
): Notification => ({
|
||||||
|
...defaultErrorNotification,
|
||||||
|
message: `Failed to delete demo data bucket: ${bucketName}: ${error}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const demoDataSucceeded = (
|
||||||
|
bucketName: string,
|
||||||
|
link: string
|
||||||
|
): Notification => ({
|
||||||
|
...defaultSuccessNotification,
|
||||||
|
message: `Successfully added demodata bucket ${bucketName}, and demodata dashboard.`,
|
||||||
|
duration: FIFTEEN_SECONDS,
|
||||||
|
linkText: 'Go to dashboard',
|
||||||
|
link,
|
||||||
|
})
|
||||||
|
|
||||||
// Limits
|
// Limits
|
||||||
export const readWriteCardinalityLimitReached = (
|
export const readWriteCardinalityLimitReached = (
|
||||||
message: string
|
message: string
|
||||||
|
@ -537,7 +565,7 @@ export const taskUpdateSuccess = (): Notification => ({
|
||||||
|
|
||||||
export const taskImportFailed = (errorMessage: string): Notification => ({
|
export const taskImportFailed = (errorMessage: string): Notification => ({
|
||||||
...defaultErrorNotification,
|
...defaultErrorNotification,
|
||||||
duration: INFINITE,
|
duration: undefined,
|
||||||
message: `Failed to import Task: ${errorMessage}.`,
|
message: `Failed to import Task: ${errorMessage}.`,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ import {
|
||||||
import {notify, dismissNotification} from 'src/shared/actions/notifications'
|
import {notify, dismissNotification} from 'src/shared/actions/notifications'
|
||||||
|
|
||||||
import {FIVE_SECONDS} from 'src/shared/constants/index'
|
import {FIVE_SECONDS} from 'src/shared/constants/index'
|
||||||
|
|
||||||
|
import {IconFont} from '@influxdata/clockface'
|
||||||
import {NotificationStyle} from 'src/types/notifications'
|
import {NotificationStyle} from 'src/types/notifications'
|
||||||
|
|
||||||
const notificationID = '000'
|
const notificationID = '000'
|
||||||
|
@ -15,7 +17,7 @@ const exampleNotification = {
|
||||||
style: NotificationStyle.Success,
|
style: NotificationStyle.Success,
|
||||||
message: 'Hell yeah you are a real notification!',
|
message: 'Hell yeah you are a real notification!',
|
||||||
duration: FIVE_SECONDS,
|
duration: FIVE_SECONDS,
|
||||||
icon: 'zap',
|
icon: IconFont.Zap,
|
||||||
}
|
}
|
||||||
|
|
||||||
const exampleNotifications = [exampleNotification]
|
const exampleNotifications = [exampleNotification]
|
||||||
|
@ -41,7 +43,7 @@ describe('Shared.Reducers.notifications', () => {
|
||||||
style: NotificationStyle.Error,
|
style: NotificationStyle.Error,
|
||||||
message: 'new notification',
|
message: 'new notification',
|
||||||
duration: FIVE_SECONDS,
|
duration: FIVE_SECONDS,
|
||||||
icon: 'zap',
|
icon: IconFont.Zap,
|
||||||
}
|
}
|
||||||
|
|
||||||
const actual = notificationsReducer(
|
const actual = notificationsReducer(
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
@import 'src/shared/components/ColorDropdown.scss';
|
@import 'src/shared/components/ColorDropdown.scss';
|
||||||
@import 'src/shared/components/avatar/Avatar.scss';
|
@import 'src/shared/components/avatar/Avatar.scss';
|
||||||
@import 'src/shared/components/tables/TableGraphs.scss';
|
@import 'src/shared/components/tables/TableGraphs.scss';
|
||||||
@import 'src/shared/components/notifications/Notifications.scss';
|
|
||||||
@import 'src/shared/components/graph_tips/GraphTips.scss';
|
@import 'src/shared/components/graph_tips/GraphTips.scss';
|
||||||
@import 'src/shared/components/cells/Dashboards.scss';
|
@import 'src/shared/components/cells/Dashboards.scss';
|
||||||
@import 'src/shared/components/code_mirror/CodeMirror.scss';
|
@import 'src/shared/components/code_mirror/CodeMirror.scss';
|
||||||
|
@ -123,12 +122,13 @@
|
||||||
@import 'src/clientLibraries/components/ClientLibraryOverlay.scss';
|
@import 'src/clientLibraries/components/ClientLibraryOverlay.scss';
|
||||||
@import 'src/dashboards/components/DashboardsCardGrid.scss';
|
@import 'src/dashboards/components/DashboardsCardGrid.scss';
|
||||||
@import 'src/dashboards/components/DashboardLightMode.scss';
|
@import 'src/dashboards/components/DashboardLightMode.scss';
|
||||||
|
@import 'src/buckets/components/DemoDataDropdown.scss';
|
||||||
|
@import 'src/shared/components/notifications/Notification.scss';
|
||||||
|
|
||||||
// External
|
// External
|
||||||
@import '../../node_modules/@influxdata/react-custom-scrollbars/dist/styles.css';
|
@import '../../node_modules/@influxdata/react-custom-scrollbars/dist/styles.css';
|
||||||
|
|
||||||
|
|
||||||
// TODO: delete this later when it's addressed in Clockface
|
// TODO: delete this later when it's addressed in Clockface
|
||||||
.cf-resource-card {
|
.cf-resource-card {
|
||||||
margin-bottom: $cf-border;
|
margin-bottom: $cf-border;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
import {Action} from 'src/shared/actions/notifications'
|
import {Action} from 'src/shared/actions/notifications'
|
||||||
|
import {IconFont} from '@influxdata/clockface'
|
||||||
|
|
||||||
export type NotificationAction = Action
|
export type NotificationAction = Action
|
||||||
|
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
id?: string
|
id?: string
|
||||||
style: NotificationStyle
|
style: NotificationStyle
|
||||||
icon: string
|
icon: IconFont
|
||||||
duration: number
|
duration?: number
|
||||||
message: string
|
message: string
|
||||||
type?: string
|
type?: string
|
||||||
|
link?: string
|
||||||
|
linkText?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum NotificationStyle {
|
export enum NotificationStyle {
|
||||||
|
|
Loading…
Reference in New Issue