feat(ui): Enhance resource creation experience when limits are reached (#19103)
* feat(ui): add AssetLimitOverlay * feat(ui): enable create bucket button * feat(ui): enable create dashboard and task buttons * feat(ui): add reusable AssetLimitButton * feat(ui): change alerts limit experience * feat(ui): update changelog * feat(ui): address review commentspull/19153/head
parent
48814abab1
commit
bc4c19dfd8
|
@ -8,15 +8,16 @@
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
1. [19075](https://github.com/influxdata/influxdb/pull/19075): Add resource links to a stack's resources from public HTTP API list/read calls
|
1. [19075](https://github.com/influxdata/influxdb/pull/19075): Add resource links to a stack's resources from public HTTP API list/read calls
|
||||||
|
1. [19103](https://github.com/influxdata/influxdb/pull/19103): Enhance resource creation experience when limits are reached
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
1. [19043](https://github.com/influxdata/influxdb/pull/19043): Enforce all influx CLI flag args are valid
|
1. [19043](https://github.com/influxdata/influxdb/pull/19043): Enforce all influx CLI flag args are valid
|
||||||
|
|
||||||
|
|
||||||
## v2.0.0-beta.15 [2020-07-23]
|
## v2.0.0-beta.15 [2020-07-23]
|
||||||
|
|
||||||
### Breaking
|
### Breaking
|
||||||
|
|
||||||
1. [19004](https://github.com/influxdata/influxdb/pull/19004): Removed the `migrate` command from the `influxd` binary.
|
1. [19004](https://github.com/influxdata/influxdb/pull/19004): Removed the `migrate` command from the `influxd` binary.
|
||||||
1. [18921](https://github.com/influxdata/influxdb/pull/18921): Restricted UI variable names to not clash with Flux reserved words
|
1. [18921](https://github.com/influxdata/influxdb/pull/18921): Restricted UI variable names to not clash with Flux reserved words
|
||||||
|
|
||||||
|
@ -41,7 +42,6 @@
|
||||||
1. [18989](https://github.com/influxdata/influxdb/pull/18989): Stopped fetching tags in the advanced builder
|
1. [18989](https://github.com/influxdata/influxdb/pull/18989): Stopped fetching tags in the advanced builder
|
||||||
1. [19044](https://github.com/influxdata/influxdb/pull/19044): Graph customization: X and Y axis properly accept values
|
1. [19044](https://github.com/influxdata/influxdb/pull/19044): Graph customization: X and Y axis properly accept values
|
||||||
|
|
||||||
|
|
||||||
## v2.0.0-beta.14 [2020-07-08]
|
## v2.0.0-beta.14 [2020-07-08]
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
|
@ -3,8 +3,8 @@ import React, {FC, ReactChild, useState} from 'react'
|
||||||
import {connect} from 'react-redux'
|
import {connect} from 'react-redux'
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import {AppState, ResourceType} from 'src/types'
|
import {AppState, ResourceType, ColumnTypes} from 'src/types'
|
||||||
import {LimitStatus} from 'src/cloud/actions/limits'
|
import {LimitStatus, MonitoringLimits} from 'src/cloud/actions/limits'
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import {
|
import {
|
||||||
|
@ -20,14 +20,17 @@ import {
|
||||||
ComponentColor,
|
ComponentColor,
|
||||||
} from '@influxdata/clockface'
|
} from '@influxdata/clockface'
|
||||||
import AssetLimitAlert from 'src/cloud/components/AssetLimitAlert'
|
import AssetLimitAlert from 'src/cloud/components/AssetLimitAlert'
|
||||||
|
import AssetLimitButton from 'src/cloud/components/AssetLimitButton'
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import {extractMonitoringLimitStatus} from 'src/cloud/utils/limits'
|
import {
|
||||||
|
extractChecksLimits,
|
||||||
|
extractRulesLimits,
|
||||||
|
extractEndpointsLimits,
|
||||||
|
} from 'src/cloud/utils/limits'
|
||||||
|
|
||||||
type ColumnTypes =
|
// Constants
|
||||||
| ResourceType.NotificationRules
|
import {CLOUD} from 'src/shared/constants'
|
||||||
| ResourceType.NotificationEndpoints
|
|
||||||
| ResourceType.Checks
|
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
type: ColumnTypes
|
type: ColumnTypes
|
||||||
|
@ -38,7 +41,7 @@ interface OwnProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StateProps {
|
interface StateProps {
|
||||||
limitStatus: LimitStatus
|
limitStatus: MonitoringLimits
|
||||||
}
|
}
|
||||||
|
|
||||||
const AlertsColumnHeader: FC<OwnProps & StateProps> = ({
|
const AlertsColumnHeader: FC<OwnProps & StateProps> = ({
|
||||||
|
@ -53,6 +56,20 @@ const AlertsColumnHeader: FC<OwnProps & StateProps> = ({
|
||||||
|
|
||||||
const formattedTitle = title.toLowerCase().replace(' ', '-')
|
const formattedTitle = title.toLowerCase().replace(' ', '-')
|
||||||
const panelClassName = `alerting-index--column alerting-index--${formattedTitle}`
|
const panelClassName = `alerting-index--column alerting-index--${formattedTitle}`
|
||||||
|
const resourceName = title.substr(0, title.length - 1)
|
||||||
|
|
||||||
|
const isLimitExceeded =
|
||||||
|
CLOUD &&
|
||||||
|
limitStatus[type] === LimitStatus.EXCEEDED &&
|
||||||
|
type !== ResourceType.Checks
|
||||||
|
|
||||||
|
const assetLimitButton = (
|
||||||
|
<AssetLimitButton
|
||||||
|
color={ComponentColor.Secondary}
|
||||||
|
buttonText="Create"
|
||||||
|
resourceName={resourceName}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel
|
<Panel
|
||||||
|
@ -70,7 +87,7 @@ const AlertsColumnHeader: FC<OwnProps & StateProps> = ({
|
||||||
tooltipContents={questionMarkTooltipContents}
|
tooltipContents={questionMarkTooltipContents}
|
||||||
/>
|
/>
|
||||||
</FlexBox>
|
</FlexBox>
|
||||||
{createButton}
|
{isLimitExceeded ? assetLimitButton : createButton}
|
||||||
</Panel.Header>
|
</Panel.Header>
|
||||||
<div className="alerting-index--search">
|
<div className="alerting-index--search">
|
||||||
<Input
|
<Input
|
||||||
|
@ -88,7 +105,10 @@ const AlertsColumnHeader: FC<OwnProps & StateProps> = ({
|
||||||
>
|
>
|
||||||
<div className="alerting-index--list">
|
<div className="alerting-index--list">
|
||||||
{children(searchTerm)}
|
{children(searchTerm)}
|
||||||
<AssetLimitAlert resourceName={title} limitStatus={limitStatus} />
|
<AssetLimitAlert
|
||||||
|
resourceName={title}
|
||||||
|
limitStatus={limitStatus[type]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DapperScrollbars>
|
</DapperScrollbars>
|
||||||
</div>
|
</div>
|
||||||
|
@ -98,7 +118,11 @@ const AlertsColumnHeader: FC<OwnProps & StateProps> = ({
|
||||||
|
|
||||||
const mstp = ({cloud: {limits}}: AppState) => {
|
const mstp = ({cloud: {limits}}: AppState) => {
|
||||||
return {
|
return {
|
||||||
limitStatus: extractMonitoringLimitStatus(limits),
|
limitStatus: {
|
||||||
|
[ResourceType.Checks]: extractChecksLimits(limits),
|
||||||
|
[ResourceType.NotificationRules]: extractRulesLimits(limits),
|
||||||
|
[ResourceType.NotificationEndpoints]: extractEndpointsLimits(limits),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,8 @@ import React, {FC, useEffect} from 'react'
|
||||||
import {connect, ConnectedProps, useDispatch} from 'react-redux'
|
import {connect, ConnectedProps, useDispatch} from 'react-redux'
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import {
|
import {Button, IconFont, ComponentColor} from '@influxdata/clockface'
|
||||||
Button,
|
import AssetLimitButton from 'src/cloud/components/AssetLimitButton'
|
||||||
IconFont,
|
|
||||||
ComponentColor,
|
|
||||||
ComponentStatus,
|
|
||||||
} from '@influxdata/clockface'
|
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
import {checkBucketLimits, LimitStatus} from 'src/cloud/actions/limits'
|
import {checkBucketLimits, LimitStatus} from 'src/cloud/actions/limits'
|
||||||
|
@ -20,10 +16,12 @@ import {extractBucketLimits} from 'src/cloud/utils/limits'
|
||||||
// Types
|
// Types
|
||||||
import {AppState} from 'src/types'
|
import {AppState} from 'src/types'
|
||||||
|
|
||||||
type ReduxProps = ConnectedProps<typeof connector>
|
// Constants
|
||||||
type Props = ReduxProps
|
import {CLOUD} from 'src/shared/constants'
|
||||||
|
|
||||||
const CreateBucketButton: FC<Props> = ({
|
type ReduxProps = ConnectedProps<typeof connector>
|
||||||
|
|
||||||
|
const CreateBucketButton: FC<ReduxProps> = ({
|
||||||
limitStatus,
|
limitStatus,
|
||||||
onShowOverlay,
|
onShowOverlay,
|
||||||
onDismissOverlay,
|
onDismissOverlay,
|
||||||
|
@ -34,33 +32,22 @@ const CreateBucketButton: FC<Props> = ({
|
||||||
dispatch(checkBucketLimits())
|
dispatch(checkBucketLimits())
|
||||||
}, [dispatch])
|
}, [dispatch])
|
||||||
|
|
||||||
const limitExceeded = limitStatus === LimitStatus.EXCEEDED
|
const handleItemClick = (): void => {
|
||||||
const text = 'Create Bucket'
|
onShowOverlay('create-bucket', null, onDismissOverlay)
|
||||||
let titleText = 'Click to create a bucket'
|
|
||||||
let buttonStatus = ComponentStatus.Default
|
|
||||||
|
|
||||||
if (limitExceeded) {
|
|
||||||
titleText = 'This account has the maximum number of buckets allowed'
|
|
||||||
buttonStatus = ComponentStatus.Disabled
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleItemClick = (): void => {
|
if (CLOUD && limitStatus === LimitStatus.EXCEEDED) {
|
||||||
if (limitExceeded) {
|
return <AssetLimitButton resourceName="Bucket" />
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
onShowOverlay('create-bucket', null, onDismissOverlay)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
icon={IconFont.Plus}
|
icon={IconFont.Plus}
|
||||||
color={ComponentColor.Primary}
|
color={ComponentColor.Primary}
|
||||||
text={text}
|
text="Create Bucket"
|
||||||
titleText={titleText}
|
titleText="Click to create a bucket"
|
||||||
onClick={handleItemClick}
|
onClick={handleItemClick}
|
||||||
testID="Create Bucket"
|
testID="Create Bucket"
|
||||||
status={buttonStatus}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,9 @@ import {
|
||||||
ResourceType,
|
ResourceType,
|
||||||
} from 'src/types'
|
} from 'src/types'
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
import {extractChecksLimits} from 'src/cloud/utils/limits'
|
||||||
|
|
||||||
type ReduxProps = ConnectedProps<typeof connector>
|
type ReduxProps = ConnectedProps<typeof connector>
|
||||||
type Props = ReduxProps & RouteComponentProps<{orgID: string}>
|
type Props = ReduxProps & RouteComponentProps<{orgID: string}>
|
||||||
|
|
||||||
|
@ -34,6 +37,7 @@ const ChecksColumn: FunctionComponent<Props> = ({
|
||||||
},
|
},
|
||||||
rules,
|
rules,
|
||||||
endpoints,
|
endpoints,
|
||||||
|
limitStatus,
|
||||||
}) => {
|
}) => {
|
||||||
const handleCreateThreshold = () => {
|
const handleCreateThreshold = () => {
|
||||||
history.push(`/orgs/${orgID}/alerting/checks/new-threshold`)
|
history.push(`/orgs/${orgID}/alerting/checks/new-threshold`)
|
||||||
|
@ -66,6 +70,7 @@ const ChecksColumn: FunctionComponent<Props> = ({
|
||||||
|
|
||||||
const createButton = (
|
const createButton = (
|
||||||
<CreateCheckDropdown
|
<CreateCheckDropdown
|
||||||
|
limitStatus={limitStatus}
|
||||||
onCreateThreshold={handleCreateThreshold}
|
onCreateThreshold={handleCreateThreshold}
|
||||||
onCreateDeadman={handleCreateDeadman}
|
onCreateDeadman={handleCreateDeadman}
|
||||||
/>
|
/>
|
||||||
|
@ -92,6 +97,10 @@ const ChecksColumn: FunctionComponent<Props> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const mstp = (state: AppState) => {
|
const mstp = (state: AppState) => {
|
||||||
|
const {
|
||||||
|
cloud: {limits},
|
||||||
|
} = state
|
||||||
|
|
||||||
const checks = getAll<Check>(state, ResourceType.Checks)
|
const checks = getAll<Check>(state, ResourceType.Checks)
|
||||||
|
|
||||||
const endpoints = getAll<NotificationEndpoint>(
|
const endpoints = getAll<NotificationEndpoint>(
|
||||||
|
@ -108,6 +117,7 @@ const mstp = (state: AppState) => {
|
||||||
checks: sortChecksByName(checks),
|
checks: sortChecksByName(checks),
|
||||||
rules: sortRulesByName(rules),
|
rules: sortRulesByName(rules),
|
||||||
endpoints: sortEndpointsByName(endpoints),
|
endpoints: sortEndpointsByName(endpoints),
|
||||||
|
limitStatus: extractChecksLimits(limits),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,41 @@
|
||||||
// Libraries
|
// Libraries
|
||||||
import React, {FunctionComponent, MouseEvent} from 'react'
|
import React, {FunctionComponent, MouseEvent} from 'react'
|
||||||
|
import {connect, ConnectedProps} from 'react-redux'
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import {Dropdown, ComponentColor, IconFont} from '@influxdata/clockface'
|
import {Dropdown, ComponentColor, IconFont} from '@influxdata/clockface'
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
import {showOverlay, dismissOverlay} from 'src/overlays/actions/overlays'
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import {CheckType} from 'src/types'
|
import {CheckType} from 'src/types'
|
||||||
|
import {LimitStatus} from 'src/cloud/actions/limits'
|
||||||
|
|
||||||
interface Props {
|
// Constants
|
||||||
|
import {CLOUD} from 'src/shared/constants'
|
||||||
|
|
||||||
|
interface OwnProps {
|
||||||
onCreateThreshold: () => void
|
onCreateThreshold: () => void
|
||||||
onCreateDeadman: () => void
|
onCreateDeadman: () => void
|
||||||
|
limitStatus: LimitStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateCheckDropdown: FunctionComponent<Props> = ({
|
type ReduxProps = ConnectedProps<typeof connector>
|
||||||
|
|
||||||
|
const CreateCheckDropdown: FunctionComponent<OwnProps & ReduxProps> = ({
|
||||||
onCreateThreshold,
|
onCreateThreshold,
|
||||||
onCreateDeadman,
|
onCreateDeadman,
|
||||||
|
onDismissOverlay,
|
||||||
|
onShowOverlay,
|
||||||
|
limitStatus,
|
||||||
}) => {
|
}) => {
|
||||||
const handleItemClick = (type: CheckType): void => {
|
const handleItemClick = (type: CheckType): void => {
|
||||||
|
if (CLOUD && limitStatus === LimitStatus.EXCEEDED) {
|
||||||
|
onShowOverlay('asset-limit', {asset: 'Checks'}, onDismissOverlay)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'threshold') {
|
if (type === 'threshold') {
|
||||||
onCreateThreshold()
|
onCreateThreshold()
|
||||||
}
|
}
|
||||||
|
@ -69,4 +88,11 @@ const CreateCheckDropdown: FunctionComponent<Props> = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreateCheckDropdown
|
const mdtp = {
|
||||||
|
onShowOverlay: showOverlay,
|
||||||
|
onDismissOverlay: dismissOverlay,
|
||||||
|
}
|
||||||
|
|
||||||
|
const connector = connect(null, mdtp)
|
||||||
|
|
||||||
|
export default connector(CreateCheckDropdown)
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
Limits,
|
Limits,
|
||||||
RemoteDataState,
|
RemoteDataState,
|
||||||
ResourceType,
|
ResourceType,
|
||||||
|
ColumnTypes,
|
||||||
} from 'src/types'
|
} from 'src/types'
|
||||||
|
|
||||||
// Selectors
|
// Selectors
|
||||||
|
@ -36,6 +37,10 @@ export enum LimitStatus {
|
||||||
EXCEEDED = 'exceeded',
|
EXCEEDED = 'exceeded',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MonitoringLimits = {
|
||||||
|
[type in ColumnTypes]: LimitStatus
|
||||||
|
}
|
||||||
|
|
||||||
export enum ActionTypes {
|
export enum ActionTypes {
|
||||||
SetLimits = 'SET_LIMITS',
|
SetLimits = 'SET_LIMITS',
|
||||||
SetLimitsStatus = 'SET_LIMITS_STATUS',
|
SetLimitsStatus = 'SET_LIMITS_STATUS',
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
// Libraries
|
||||||
|
import React, {FC} from 'react'
|
||||||
|
import {connect, ConnectedProps} from 'react-redux'
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import {Button, ComponentColor, IconFont} from '@influxdata/clockface'
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
import {showOverlay, dismissOverlay} from 'src/overlays/actions/overlays'
|
||||||
|
|
||||||
|
interface OwnProps {
|
||||||
|
color?: ComponentColor
|
||||||
|
resourceName: string
|
||||||
|
buttonText?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReduxProps = ConnectedProps<typeof connector>
|
||||||
|
|
||||||
|
const AssetLimitButton: FC<OwnProps & ReduxProps> = ({
|
||||||
|
color = ComponentColor.Primary,
|
||||||
|
buttonText,
|
||||||
|
resourceName,
|
||||||
|
onShowOverlay,
|
||||||
|
onDismissOverlay,
|
||||||
|
}) => {
|
||||||
|
const handleClick = () => {
|
||||||
|
onShowOverlay('asset-limit', {asset: `${resourceName}s`}, onDismissOverlay)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
icon={IconFont.Plus}
|
||||||
|
text={buttonText || `Create ${resourceName}`}
|
||||||
|
color={color}
|
||||||
|
titleText={`Click to create ${resourceName}`}
|
||||||
|
onClick={handleClick}
|
||||||
|
testID={`create-${resourceName.toLowerCase()}`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mdtp = {
|
||||||
|
onShowOverlay: showOverlay,
|
||||||
|
onDismissOverlay: dismissOverlay,
|
||||||
|
}
|
||||||
|
|
||||||
|
const connector = connect(null, mdtp)
|
||||||
|
|
||||||
|
export default connector(AssetLimitButton)
|
|
@ -0,0 +1,71 @@
|
||||||
|
// Libraries
|
||||||
|
import React, {FC} from 'react'
|
||||||
|
import {connect} from 'react-redux'
|
||||||
|
import {get} from 'lodash'
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import {
|
||||||
|
OverlayContainer,
|
||||||
|
Overlay,
|
||||||
|
ComponentSize,
|
||||||
|
GradientBox,
|
||||||
|
Gradients,
|
||||||
|
InfluxColors,
|
||||||
|
} from '@influxdata/clockface'
|
||||||
|
import CloudUpgradeButton from 'src/shared/components/CloudUpgradeButton'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import {AppState} from 'src/types'
|
||||||
|
|
||||||
|
interface OwnProps {
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StateProps {
|
||||||
|
assetName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const AssetLimitOverlay: FC<OwnProps & StateProps> = ({assetName, onClose}) => {
|
||||||
|
return (
|
||||||
|
<OverlayContainer
|
||||||
|
maxWidth={600}
|
||||||
|
testID="asset-limit-overlay"
|
||||||
|
className="asset-limit-overlay"
|
||||||
|
>
|
||||||
|
<GradientBox
|
||||||
|
borderGradient={Gradients.MiyazakiSky}
|
||||||
|
borderColor={InfluxColors.Raven}
|
||||||
|
>
|
||||||
|
<div className="asset-limit-overlay--contents">
|
||||||
|
<Overlay.Header
|
||||||
|
title={`Need More ${assetName}?`}
|
||||||
|
wrapText={true}
|
||||||
|
onDismiss={onClose}
|
||||||
|
/>
|
||||||
|
<Overlay.Body>
|
||||||
|
<div className="asset-limit-overlay--graphic-container">
|
||||||
|
<div className="asset-limit-overlay--graphic">
|
||||||
|
<div className="asset-limit-overlay--graphic-element" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
You’ve reached the maximum allotted {assetName} on your current
|
||||||
|
plan. Upgrade to Pay as you go to create more {assetName}.
|
||||||
|
</p>
|
||||||
|
</Overlay.Body>
|
||||||
|
<Overlay.Footer>
|
||||||
|
<CloudUpgradeButton size={ComponentSize.Large} />
|
||||||
|
</Overlay.Footer>
|
||||||
|
</div>
|
||||||
|
</GradientBox>
|
||||||
|
</OverlayContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mstp = (state: AppState) => {
|
||||||
|
return {
|
||||||
|
assetName: get(state, 'overlays.params.asset', ''),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mstp)(AssetLimitOverlay)
|
|
@ -34,10 +34,16 @@ export const extractTaskMax = (limits: LimitsState): number => {
|
||||||
return get(limits, 'tasks.maxAllowed') || Infinity
|
return get(limits, 'tasks.maxAllowed') || Infinity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const extractChecksLimits = (limits: LimitsState): LimitStatus => {
|
||||||
|
return get(limits, 'checks.limitStatus')
|
||||||
|
}
|
||||||
export const extractChecksMax = (limits: LimitsState): number => {
|
export const extractChecksMax = (limits: LimitsState): number => {
|
||||||
return get(limits, 'checks.maxAllowed') || Infinity
|
return get(limits, 'checks.maxAllowed') || Infinity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const extractRulesLimits = (limits: LimitsState): LimitStatus => {
|
||||||
|
return get(limits, 'rules.limitStatus')
|
||||||
|
}
|
||||||
export const extractRulesMax = (limits: LimitsState): number => {
|
export const extractRulesMax = (limits: LimitsState): number => {
|
||||||
return get(limits, 'rules.maxAllowed') || Infinity
|
return get(limits, 'rules.maxAllowed') || Infinity
|
||||||
}
|
}
|
||||||
|
@ -45,6 +51,9 @@ export const extractBlockedRules = (limits: LimitsState): string[] => {
|
||||||
return get(limits, 'rules.blocked') || []
|
return get(limits, 'rules.blocked') || []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const extractEndpointsLimits = (limits: LimitsState): LimitStatus => {
|
||||||
|
return get(limits, 'endpoints.limitStatus')
|
||||||
|
}
|
||||||
export const extractEndpointsMax = (limits: LimitsState): number => {
|
export const extractEndpointsMax = (limits: LimitsState): number => {
|
||||||
return get(limits, 'endpoints.maxAllowed') || Infinity
|
return get(limits, 'endpoints.maxAllowed') || Infinity
|
||||||
}
|
}
|
||||||
|
@ -52,22 +61,6 @@ export const extractBlockedEndpoints = (limits: LimitsState): string[] => {
|
||||||
return get(limits, 'endpoints.blocked') || []
|
return get(limits, 'endpoints.blocked') || []
|
||||||
}
|
}
|
||||||
|
|
||||||
export const extractMonitoringLimitStatus = (
|
|
||||||
limits: LimitsState
|
|
||||||
): LimitStatus => {
|
|
||||||
const statuses = [
|
|
||||||
get(limits, 'rules.limitStatus'),
|
|
||||||
get(limits, 'endpoints.limitStatus'),
|
|
||||||
get(limits, 'checks.limitStatus'),
|
|
||||||
]
|
|
||||||
|
|
||||||
if (statuses.includes(LimitStatus.EXCEEDED)) {
|
|
||||||
return LimitStatus.EXCEEDED
|
|
||||||
}
|
|
||||||
|
|
||||||
return LimitStatus.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
export const extractLimitedMonitoringResources = (
|
export const extractLimitedMonitoringResources = (
|
||||||
limits: LimitsState
|
limits: LimitsState
|
||||||
): string => {
|
): string => {
|
||||||
|
|
|
@ -29,8 +29,7 @@ import {setDashboardSort} from 'src/dashboards/actions/creators'
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import {AppState, ResourceType} from 'src/types'
|
import {AppState, ResourceType} from 'src/types'
|
||||||
import {LimitStatus} from 'src/cloud/actions/limits'
|
import {Sort} from '@influxdata/clockface'
|
||||||
import {ComponentStatus, Sort} from '@influxdata/clockface'
|
|
||||||
import {SortTypes} from 'src/shared/utils/sort'
|
import {SortTypes} from 'src/shared/utils/sort'
|
||||||
import {DashboardSortKey} from 'src/shared/components/resource_sort_dropdown/generateSortItems'
|
import {DashboardSortKey} from 'src/shared/components/resource_sort_dropdown/generateSortItems'
|
||||||
|
|
||||||
|
@ -52,7 +51,7 @@ class DashboardIndex extends PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const {createDashboard, sortOptions} = this.props
|
const {createDashboard, sortOptions, limitStatus} = this.props
|
||||||
const {searchTerm} = this.state
|
const {searchTerm} = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -87,7 +86,7 @@ class DashboardIndex extends PureComponent<Props, State> {
|
||||||
onSelectTemplate={this.summonImportFromTemplateOverlay}
|
onSelectTemplate={this.summonImportFromTemplateOverlay}
|
||||||
resourceName="Dashboard"
|
resourceName="Dashboard"
|
||||||
canImportFromTemplate={true}
|
canImportFromTemplate={true}
|
||||||
status={this.addResourceStatus}
|
limitStatus={limitStatus}
|
||||||
/>
|
/>
|
||||||
</Page.ControlBarRight>
|
</Page.ControlBarRight>
|
||||||
</Page.ControlBar>
|
</Page.ControlBar>
|
||||||
|
@ -156,14 +155,6 @@ class DashboardIndex extends PureComponent<Props, State> {
|
||||||
} = this.props
|
} = this.props
|
||||||
history.push(`/orgs/${orgID}/dashboards-list/import/template`)
|
history.push(`/orgs/${orgID}/dashboards-list/import/template`)
|
||||||
}
|
}
|
||||||
|
|
||||||
private get addResourceStatus(): ComponentStatus {
|
|
||||||
const {limitStatus} = this.props
|
|
||||||
if (limitStatus === LimitStatus.EXCEEDED) {
|
|
||||||
return ComponentStatus.Disabled
|
|
||||||
}
|
|
||||||
return ComponentStatus.Default
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mstp = (state: AppState) => {
|
const mstp = (state: AppState) => {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import TelegrafConfigOverlay from 'src/telegrafs/components/TelegrafConfigOverla
|
||||||
import TelegrafOutputOverlay from 'src/telegrafs/components/TelegrafOutputOverlay'
|
import TelegrafOutputOverlay from 'src/telegrafs/components/TelegrafOutputOverlay'
|
||||||
import OrgSwitcherOverlay from 'src/pageLayout/components/OrgSwitcherOverlay'
|
import OrgSwitcherOverlay from 'src/pageLayout/components/OrgSwitcherOverlay'
|
||||||
import CreateBucketOverlay from 'src/buckets/components/CreateBucketOverlay'
|
import CreateBucketOverlay from 'src/buckets/components/CreateBucketOverlay'
|
||||||
|
import AssetLimitOverlay from 'src/cloud/components/AssetLimitOverlay'
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
import {dismissOverlay} from 'src/overlays/actions/overlays'
|
import {dismissOverlay} from 'src/overlays/actions/overlays'
|
||||||
|
@ -57,6 +58,9 @@ const OverlayController: FunctionComponent<OverlayControllerProps> = props => {
|
||||||
case 'create-bucket':
|
case 'create-bucket':
|
||||||
activeOverlay = <CreateBucketOverlay onClose={closer} />
|
activeOverlay = <CreateBucketOverlay onClose={closer} />
|
||||||
break
|
break
|
||||||
|
case 'asset-limit':
|
||||||
|
activeOverlay = <AssetLimitOverlay onClose={closer} />
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
visibility = false
|
visibility = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ export type OverlayID =
|
||||||
| 'telegraf-output'
|
| 'telegraf-output'
|
||||||
| 'switch-organizations'
|
| 'switch-organizations'
|
||||||
| 'create-bucket'
|
| 'create-bucket'
|
||||||
|
| 'asset-limit'
|
||||||
|
|
||||||
export interface OverlayParams {
|
export interface OverlayParams {
|
||||||
[key: string]: string
|
[key: string]: string
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// Libraries
|
// Libraries
|
||||||
import React, {PureComponent} from 'react'
|
import React, {PureComponent} from 'react'
|
||||||
|
import {connect, ConnectedProps} from 'react-redux'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
|
@ -11,11 +12,21 @@ import {
|
||||||
ComponentStatus,
|
ComponentStatus,
|
||||||
} from '@influxdata/clockface'
|
} from '@influxdata/clockface'
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
import {showOverlay, dismissOverlay} from 'src/overlays/actions/overlays'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import {LimitStatus} from 'src/cloud/actions/limits'
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
import {CLOUD} from 'src/shared/constants'
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
onSelectNew: () => void
|
onSelectNew: () => void
|
||||||
onSelectImport: () => void
|
onSelectImport: () => void
|
||||||
onSelectTemplate?: () => void
|
onSelectTemplate?: () => void
|
||||||
resourceName: string
|
resourceName: string
|
||||||
|
limitStatus?: LimitStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DefaultProps {
|
interface DefaultProps {
|
||||||
|
@ -24,9 +35,11 @@ interface DefaultProps {
|
||||||
titleText: string
|
titleText: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = OwnProps & DefaultProps
|
type ReduxProps = ConnectedProps<typeof connector>
|
||||||
|
|
||||||
export default class AddResourceDropdown extends PureComponent<Props> {
|
type Props = OwnProps & DefaultProps & ReduxProps
|
||||||
|
|
||||||
|
class AddResourceDropdown extends PureComponent<Props> {
|
||||||
public static defaultProps: DefaultProps = {
|
public static defaultProps: DefaultProps = {
|
||||||
canImportFromTemplate: false,
|
canImportFromTemplate: false,
|
||||||
status: ComponentStatus.Default,
|
status: ComponentStatus.Default,
|
||||||
|
@ -121,8 +134,23 @@ export default class AddResourceDropdown extends PureComponent<Props> {
|
||||||
return `From a Template`
|
return `From a Template`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleLimit = (): void => {
|
||||||
|
const {resourceName, onShowOverlay, onDismissOverlay} = this.props
|
||||||
|
onShowOverlay('asset-limit', {asset: `${resourceName}s`}, onDismissOverlay)
|
||||||
|
}
|
||||||
|
|
||||||
private handleSelect = (selection: string): void => {
|
private handleSelect = (selection: string): void => {
|
||||||
const {onSelectNew, onSelectImport, onSelectTemplate} = this.props
|
const {
|
||||||
|
onSelectNew,
|
||||||
|
onSelectImport,
|
||||||
|
onSelectTemplate,
|
||||||
|
limitStatus = LimitStatus.OK,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
if (CLOUD && limitStatus === LimitStatus.EXCEEDED) {
|
||||||
|
this.handleLimit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (selection === this.newOption) {
|
if (selection === this.newOption) {
|
||||||
onSelectNew()
|
onSelectNew()
|
||||||
|
@ -135,3 +163,12 @@ export default class AddResourceDropdown extends PureComponent<Props> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mdtp = {
|
||||||
|
onShowOverlay: showOverlay,
|
||||||
|
onDismissOverlay: dismissOverlay,
|
||||||
|
}
|
||||||
|
|
||||||
|
const connector = connect(null, mdtp)
|
||||||
|
|
||||||
|
export default connector(AddResourceDropdown)
|
||||||
|
|
|
@ -30,10 +30,12 @@ interface StateProps {
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
className?: string
|
className?: string
|
||||||
buttonText?: string
|
buttonText?: string
|
||||||
|
size?: ComponentSize
|
||||||
}
|
}
|
||||||
|
|
||||||
const CloudUpgradeButton: FC<StateProps & OwnProps> = ({
|
const CloudUpgradeButton: FC<StateProps & OwnProps> = ({
|
||||||
inView,
|
inView,
|
||||||
|
size = ComponentSize.Small,
|
||||||
className,
|
className,
|
||||||
buttonText = 'Upgrade Now',
|
buttonText = 'Upgrade Now',
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -47,7 +49,7 @@ const CloudUpgradeButton: FC<StateProps & OwnProps> = ({
|
||||||
<LinkButton
|
<LinkButton
|
||||||
className={cloudUpgradeButtonClass}
|
className={cloudUpgradeButtonClass}
|
||||||
color={ComponentColor.Success}
|
color={ComponentColor.Success}
|
||||||
size={ComponentSize.Small}
|
size={size}
|
||||||
shape={ButtonShape.Default}
|
shape={ButtonShape.Default}
|
||||||
href={`${CLOUD_URL}${CLOUD_CHECKOUT_PATH}`}
|
href={`${CLOUD_URL}${CLOUD_CHECKOUT_PATH}`}
|
||||||
target="_self"
|
target="_self"
|
||||||
|
|
|
@ -1,67 +1,119 @@
|
||||||
.load-data--asset-alert {
|
.load-data--asset-alert {
|
||||||
margin-bottom: $cf-marg-d;
|
margin-bottom: $cf-marg-d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cf-page-control-bar {
|
.cf-page-control-bar {
|
||||||
+ .rate-alert {
|
+ .rate-alert {
|
||||||
margin: 0 $cf-marg-c $cf-marg-c;
|
margin: 0 $cf-marg-c $cf-marg-c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rate-alert--content {
|
.rate-alert--content {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
.rate-alert--button {
|
.rate-alert--button {
|
||||||
margin-top: $cf-marg-b;
|
margin-top: $cf-marg-b;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cf-page--title {
|
.cf-page--title {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-alert {
|
.asset-alert {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-position: bottom -20px left -20px;
|
background-position: bottom -20px left -20px;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-image: url('../../assets/images/dashboard-empty--dark.svg');
|
background-image: url('~assets/images/dashboard-empty--dark.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboards--asset-alert {
|
.dashboards--asset-alert {
|
||||||
.asset-alert--contents {
|
.asset-alert--contents {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a.upgrade-payg--button {
|
a.upgrade-payg--button {
|
||||||
@include gradient-diag-up($c-pool, $c-rainforest);
|
@include gradient-diag-up($c-pool, $c-rainforest);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-limit-overlay--contents {
|
||||||
|
background-color: $g1-raven;
|
||||||
|
border-radius: $cf-radius;
|
||||||
|
p {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.cf-overlay--title {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-limit-overlay--graphic-container {
|
||||||
|
width: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-limit-overlay--graphic {
|
||||||
|
display: inline-flex;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-limit-overlay--graphic:after {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
padding-bottom: 57.3170731707317%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-limit-overlay--graphic-element {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-position: center center;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-image: url('~assets/images/dashboard-empty--dark.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: $cf-nav-menu--breakpoint) {
|
@media screen and (min-width: $cf-nav-menu--breakpoint) {
|
||||||
.rate-alert {
|
.rate-alert {
|
||||||
margin-left: $cf-marg-d;
|
margin-left: $cf-marg-d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rate-alert--content {
|
.rate-alert--content {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
.rate-alert--button {
|
.rate-alert--button {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-left: $cf-marg-c;
|
margin-left: $cf-marg-c;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.cf-page-control-bar {
|
.cf-page-control-bar {
|
||||||
+ .rate-alert {
|
+ .rate-alert {
|
||||||
margin: 0 $cf-marg-d $cf-marg-c;
|
margin: 0 $cf-marg-d $cf-marg-c;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-limit-overlay--graphic {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-limit-overlay--contents {
|
||||||
|
.cf-overlay--header__wrap {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -6,7 +6,6 @@ import {
|
||||||
InputLabel,
|
InputLabel,
|
||||||
SlideToggle,
|
SlideToggle,
|
||||||
ComponentSize,
|
ComponentSize,
|
||||||
ComponentStatus,
|
|
||||||
Page,
|
Page,
|
||||||
Sort,
|
Sort,
|
||||||
FlexBox,
|
FlexBox,
|
||||||
|
@ -57,6 +56,7 @@ export default class TasksHeader extends PureComponent<Props> {
|
||||||
sortType,
|
sortType,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
onSort,
|
onSort,
|
||||||
|
limitStatus,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -98,19 +98,11 @@ export default class TasksHeader extends PureComponent<Props> {
|
||||||
onSelectImport={onImportTask}
|
onSelectImport={onImportTask}
|
||||||
onSelectTemplate={onImportFromTemplate}
|
onSelectTemplate={onImportFromTemplate}
|
||||||
resourceName="Task"
|
resourceName="Task"
|
||||||
status={this.addResourceStatus}
|
limitStatus={limitStatus}
|
||||||
/>
|
/>
|
||||||
</Page.ControlBarRight>
|
</Page.ControlBarRight>
|
||||||
</Page.ControlBar>
|
</Page.ControlBar>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private get addResourceStatus(): ComponentStatus {
|
|
||||||
const {limitStatus} = this.props
|
|
||||||
if (limitStatus === LimitStatus.EXCEEDED) {
|
|
||||||
return ComponentStatus.Disabled
|
|
||||||
}
|
|
||||||
return ComponentStatus.Default
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,9 @@ import {
|
||||||
CheckBase as GenCheckBase,
|
CheckBase as GenCheckBase,
|
||||||
NotificationEndpointBase as GenEndpointBase,
|
NotificationEndpointBase as GenEndpointBase,
|
||||||
} from 'src/client'
|
} from 'src/client'
|
||||||
|
|
||||||
import {RemoteDataState} from 'src/types'
|
import {RemoteDataState} from 'src/types'
|
||||||
|
import {ResourceType} from './resources'
|
||||||
|
|
||||||
type Omit<T, U> = Pick<T, Exclude<keyof T, U>>
|
type Omit<T, U> = Pick<T, Exclude<keyof T, U>>
|
||||||
type Overwrite<T, U> = Omit<T, keyof U> & U
|
type Overwrite<T, U> = Omit<T, keyof U> & U
|
||||||
|
@ -30,6 +32,11 @@ interface WithClientID<T> {
|
||||||
value: T
|
value: T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ColumnTypes =
|
||||||
|
| ResourceType.NotificationRules
|
||||||
|
| ResourceType.NotificationEndpoints
|
||||||
|
| ResourceType.Checks
|
||||||
|
|
||||||
/* Endpoints */
|
/* Endpoints */
|
||||||
type EndpointOverrides = {
|
type EndpointOverrides = {
|
||||||
status: RemoteDataState
|
status: RemoteDataState
|
||||||
|
|
Loading…
Reference in New Issue