feat(notifications): create notification endpoint overlay (#14693)
* chore: update swagger definitions * feat(endpoints): introducer endpoint overlay * wip: change endpoint type * feat(endpoint): add inputs for supported endpoint types * wip: connect create endpoint to API * feat: implement create endpoint * alerts(e2e): add createEndpoint command * chore: update swaggerpull/14713/head
parent
8ef3d9e94c
commit
286d57b0ba
|
@ -9205,14 +9205,12 @@ components:
|
|||
NotificationEndpoint:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/SlackNotificationEndpoint"
|
||||
- $ref: "#/components/schemas/SMTPNotificationEndpoint"
|
||||
- $ref: "#/components/schemas/PagerDutyNotificationEndpoint"
|
||||
- $ref: "#/components/schemas/WebhookNotificationEndpoint"
|
||||
discriminator:
|
||||
propertyName: type
|
||||
mapping:
|
||||
slack: "#/components/schemas/SlackNotificationEndpoint"
|
||||
smtp: "#/components/schemas/SMTPNotificationEndpoint"
|
||||
pagerduty: "#/components/schemas/PagerDutyNotificationEndpoint"
|
||||
webhook: "#/components/schemas/WebhookNotificationEndpoint"
|
||||
NotificationEndpoints:
|
||||
|
@ -9225,6 +9223,7 @@ components:
|
|||
$ref: "#/components/schemas/Links"
|
||||
NotificationEndpointBase:
|
||||
type: object
|
||||
required: [type]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
|
@ -9254,30 +9253,54 @@ components:
|
|||
$ref: "#/components/schemas/Labels"
|
||||
type:
|
||||
$ref: "#/components/schemas/NotificationEndpointType"
|
||||
required: [type]
|
||||
SlackNotificationEndpoint:
|
||||
type: object
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/NotificationEndpointBase"
|
||||
- type: object
|
||||
SMTPNotificationEndpoint:
|
||||
type: object
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/NotificationEndpointBase"
|
||||
- type: object
|
||||
required: [url, token]
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
PagerDutyNotificationEndpoint:
|
||||
type: object
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/NotificationEndpointBase"
|
||||
- type: object
|
||||
required: [url, routingKey]
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
routingKey:
|
||||
type: string
|
||||
WebhookNotificationEndpoint:
|
||||
type: object
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/NotificationEndpointBase"
|
||||
- type: object
|
||||
required: [url, authmethod, method]
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
method:
|
||||
type: string
|
||||
enum: ['POST', 'GET', 'PUT']
|
||||
authmethod:
|
||||
type: string
|
||||
enum: ['none', 'basic', 'bearer']
|
||||
contentTemplate:
|
||||
type: string
|
||||
NotificationEndpointType:
|
||||
type: string
|
||||
enum: ['slack', smtp, 'pagerduty', 'webhook']
|
||||
enum: ['slack', 'pagerduty', 'webhook']
|
||||
securitySchemes:
|
||||
BasicAuth:
|
||||
type: http
|
||||
|
|
|
@ -13,7 +13,7 @@ describe('Dashboards', () => {
|
|||
})
|
||||
|
||||
cy.fixture('routes').then(({orgs}) => {
|
||||
cy.get<Organization>('@org').then(({id}) => {
|
||||
cy.get('@org').then(({id}: Organization) => {
|
||||
cy.visit(`${orgs}/${id}/dashboards`)
|
||||
})
|
||||
})
|
||||
|
@ -27,7 +27,7 @@ describe('Dashboards', () => {
|
|||
cy.getByTestID('add-resource-dropdown--new').click()
|
||||
|
||||
cy.fixture('routes').then(({orgs}) => {
|
||||
cy.get<Organization>('@org').then(({id}) => {
|
||||
cy.get('@org').then(({id}: Organization) => {
|
||||
cy.visit(`${orgs}/${id}/dashboards`)
|
||||
})
|
||||
})
|
||||
|
@ -41,7 +41,7 @@ describe('Dashboards', () => {
|
|||
cy.getByTestID('add-resource-dropdown--new').click()
|
||||
|
||||
cy.fixture('routes').then(({orgs}) => {
|
||||
cy.get<Organization>('@org').then(({id}) => {
|
||||
cy.get('@org').then(({id}: Organization) => {
|
||||
cy.visit(`${orgs}/${id}/dashboards`)
|
||||
})
|
||||
})
|
||||
|
@ -51,7 +51,7 @@ describe('Dashboards', () => {
|
|||
|
||||
it.only('can create a dashboard from a Template', () => {
|
||||
cy.getByTestID('dashboard-card').should('have.length', 0)
|
||||
cy.get<Organization>('@org').then(({id}) => {
|
||||
cy.get('@org').then(({id}: Organization) => {
|
||||
cy.createDashboardTemplate(id)
|
||||
})
|
||||
|
||||
|
@ -70,7 +70,7 @@ describe('Dashboards', () => {
|
|||
|
||||
describe('Dashboard List', () => {
|
||||
beforeEach(() => {
|
||||
cy.get<Organization>('@org').then(({id}) => {
|
||||
cy.get('@org').then(({id}: Organization) => {
|
||||
cy.createDashboard(id, dashboardName).then(({body}) => {
|
||||
cy.createAndAddLabel('dashboards', id, body.id, newLabelName)
|
||||
})
|
||||
|
@ -81,7 +81,7 @@ describe('Dashboards', () => {
|
|||
})
|
||||
|
||||
cy.fixture('routes').then(({orgs}) => {
|
||||
cy.get<Organization>('@org').then(({id}) => {
|
||||
cy.get('@org').then(({id}: Organization) => {
|
||||
cy.visit(`${orgs}/${id}/dashboards`)
|
||||
})
|
||||
})
|
||||
|
@ -152,7 +152,7 @@ describe('Dashboards', () => {
|
|||
it('can add an existing label to a dashboard', () => {
|
||||
const labelName = 'swogglez'
|
||||
|
||||
cy.get<Organization>('@org').then(({id}) => {
|
||||
cy.get('@org').then(({id}: Organization) => {
|
||||
cy.createLabel(labelName, id).then(() => {
|
||||
cy.getByTestID(`inline-labels--add`)
|
||||
.first()
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
describe('Notification Endpoints', () => {
|
||||
beforeEach(() => {
|
||||
cy.flush()
|
||||
|
||||
cy.signin().then(({body}) => {
|
||||
const {
|
||||
org: {id},
|
||||
} = body
|
||||
cy.wrap(body.org).as('org')
|
||||
cy.fixture('routes').then(({orgs, alerting}) => {
|
||||
cy.visit(`${orgs}/${id}${alerting}`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('can create a notification endpoint', () => {
|
||||
const name = 'An Endpoint Has No Name'
|
||||
const description =
|
||||
'A minute, an hour, a month. Notification Endpoint is certain. The time is not.'
|
||||
|
||||
cy.getByTestID('alert-column--header create-endpoint').click()
|
||||
|
||||
cy.getByTestID('endpoint-name--input')
|
||||
.clear()
|
||||
.type(name)
|
||||
.should('have.value', name)
|
||||
|
||||
cy.getByTestID('endpoint-description--textarea')
|
||||
.clear()
|
||||
.type(description)
|
||||
.should('have.value', description)
|
||||
|
||||
cy.getByTestID('endpoint-change--dropdown')
|
||||
.click()
|
||||
.within(() => {
|
||||
cy.getByTestID('endpoint--dropdown--button').within(() => {
|
||||
cy.contains('Slack')
|
||||
})
|
||||
|
||||
cy.getByTestID('endpoint--dropdown-item pagerduty').click()
|
||||
|
||||
cy.getByTestID('endpoint--dropdown--button').within(() => {
|
||||
cy.contains('Pagerduty')
|
||||
})
|
||||
})
|
||||
|
||||
cy.getByTestID('pagerduty-url')
|
||||
.clear()
|
||||
.type('many-faced-god.gov')
|
||||
.should('have.value', 'many-faced-god.gov')
|
||||
|
||||
cy.getByTestID('pagerduty-routing-key')
|
||||
.type('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9')
|
||||
.should('have.value', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9')
|
||||
|
||||
cy.getByTestID('endpoint-change--dropdown')
|
||||
.click()
|
||||
.within(() => {
|
||||
cy.getByTestID('endpoint--dropdown--button').within(() => {
|
||||
cy.contains('Pagerduty')
|
||||
})
|
||||
|
||||
cy.getByTestID('endpoint--dropdown-item slack').click()
|
||||
|
||||
cy.getByTestID('endpoint--dropdown--button').within(() => {
|
||||
cy.contains('Slack')
|
||||
})
|
||||
})
|
||||
|
||||
cy.getByTestID('slack-url')
|
||||
.clear()
|
||||
.type('slack.url.us')
|
||||
.should('have.value', 'slack.url.us')
|
||||
|
||||
cy.getByTestID('slack-token')
|
||||
.clear()
|
||||
.type('another token')
|
||||
.should('have.value', 'another token')
|
||||
|
||||
cy.getByTestID('endpoint-save--button').click()
|
||||
|
||||
cy.getByTestID(`endpoint-card ${name}`).should('exist')
|
||||
cy.getByTestID('endpoint--overlay').should('not.be.visible')
|
||||
})
|
||||
})
|
|
@ -22,6 +22,7 @@ import {
|
|||
createDashboardTemplate,
|
||||
writeData,
|
||||
getByTestIDSubStr,
|
||||
createEndpoint,
|
||||
} from './support/commands'
|
||||
|
||||
declare global {
|
||||
|
@ -49,6 +50,7 @@ declare global {
|
|||
createTelegraf: typeof createTelegraf
|
||||
createToken: typeof createToken
|
||||
writeData: typeof writeData
|
||||
createEndpoint: typeof createEndpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import {NotificationEndpoint} from '../../src/types'
|
||||
|
||||
export const signin = (): Cypress.Chainable<Cypress.Response> => {
|
||||
return cy.fixture('user').then(({username, password}) => {
|
||||
return cy.setupUser().then(body => {
|
||||
|
@ -359,6 +361,27 @@ export const fluxEqual = (s1: string, s2: string): Cypress.Chainable => {
|
|||
return cy.wrap(strip1 === strip2)
|
||||
}
|
||||
|
||||
// notification endpoints
|
||||
export const createEndpoint = (
|
||||
name: string,
|
||||
orgID: string
|
||||
): Cypress.Chainable<Cypress.Response> => {
|
||||
const endpoint: NotificationEndpoint = {
|
||||
orgID,
|
||||
name,
|
||||
userID: '',
|
||||
description: 'interrupt everyone at work',
|
||||
status: 'active',
|
||||
type: 'slack',
|
||||
url: 'insert.slack.url.here',
|
||||
}
|
||||
|
||||
return cy.request('POST', 'api/v2/notificationEndpoints', endpoint)
|
||||
}
|
||||
|
||||
// notification endpoints
|
||||
Cypress.Commands.add('createEndpoint', createEndpoint)
|
||||
|
||||
// assertions
|
||||
Cypress.Commands.add('fluxEqual', fluxEqual)
|
||||
|
||||
|
@ -397,15 +420,15 @@ Cypress.Commands.add('flush', flush)
|
|||
// tasks
|
||||
Cypress.Commands.add('createTask', createTask)
|
||||
|
||||
//Tokems
|
||||
// tokens
|
||||
Cypress.Commands.add('createToken', createToken)
|
||||
|
||||
// variables
|
||||
Cypress.Commands.add('createVariable', createVariable)
|
||||
|
||||
// Labels
|
||||
// labels
|
||||
Cypress.Commands.add('createLabel', createLabel)
|
||||
Cypress.Commands.add('createAndAddLabel', createAndAddLabel)
|
||||
|
||||
//Test
|
||||
// test
|
||||
Cypress.Commands.add('writeData', writeData)
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
// Libraries
|
||||
import {Dispatch} from 'react'
|
||||
|
||||
// Types
|
||||
import {NotificationEndpoint, GetState} from 'src/types'
|
||||
import {RemoteDataState} from '@influxdata/clockface'
|
||||
|
||||
// APIs
|
||||
import * as api from 'src/client'
|
||||
|
||||
export type Action =
|
||||
| {type: 'SET_ENDPOINT'; endpoint: NotificationEndpoint}
|
||||
| {
|
||||
type: 'SET_ALL_ENDPOINTS'
|
||||
status: RemoteDataState
|
||||
endpoints?: NotificationEndpoint[]
|
||||
}
|
||||
|
||||
export const getEndpoints = () => async (
|
||||
dispatch: Dispatch<Action>,
|
||||
getState: GetState
|
||||
) => {
|
||||
try {
|
||||
dispatch({
|
||||
type: 'SET_ALL_ENDPOINTS',
|
||||
status: RemoteDataState.Loading,
|
||||
})
|
||||
|
||||
const {orgs} = getState()
|
||||
|
||||
const resp = await api.getNotificationEndpoints({
|
||||
query: {orgID: orgs.org.id},
|
||||
})
|
||||
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(resp.data.message)
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'SET_ALL_ENDPOINTS',
|
||||
status: RemoteDataState.Done,
|
||||
endpoints: resp.data.notificationEndpoints,
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
dispatch({type: 'SET_ALL_ENDPOINTS', status: RemoteDataState.Error})
|
||||
}
|
||||
}
|
||||
|
||||
export const createEndpoint = (data: NotificationEndpoint) => async (
|
||||
dispatch: Dispatch<Action>
|
||||
) => {
|
||||
const resp = await api.postNotificationEndpoint({data})
|
||||
|
||||
if (resp.status !== 201) {
|
||||
throw new Error(resp.data.message)
|
||||
}
|
||||
|
||||
const endpoint = (resp.data as unknown) as NotificationEndpoint
|
||||
|
||||
dispatch({
|
||||
type: 'SET_ENDPOINT',
|
||||
endpoint,
|
||||
})
|
||||
}
|
|
@ -1,29 +1,38 @@
|
|||
// Libraries
|
||||
import React, {FunctionComponent} from 'react'
|
||||
import React, {FC} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {withRouter, WithRouterProps} from 'react-router'
|
||||
|
||||
// Components
|
||||
import {EmptyState, ComponentSize} from '@influxdata/clockface'
|
||||
import EndpointCards from 'src/alerting/components/endpoints/EndpointCards'
|
||||
import AlertsColumn from 'src/alerting/components/AlertsColumn'
|
||||
import {AppState} from 'src/types'
|
||||
|
||||
interface StateProps {
|
||||
endpoints: AppState['endpoints']['list']
|
||||
}
|
||||
type OwnProps = {}
|
||||
type Props = OwnProps & WithRouterProps & StateProps
|
||||
|
||||
const EndpointsColumn: FC<Props> = ({router, params, endpoints}) => {
|
||||
const handleOpenOverlay = () => {
|
||||
const newRuleRoute = `/orgs/${params.orgID}/alerting/endpoints/new`
|
||||
router.push(newRuleRoute)
|
||||
}
|
||||
|
||||
const EndpointsColumn: FunctionComponent = () => {
|
||||
return (
|
||||
<AlertsColumn
|
||||
title="Notification Endpoints"
|
||||
testID="create-endpoint"
|
||||
onCreate={() => {}}
|
||||
onCreate={handleOpenOverlay}
|
||||
>
|
||||
<EmptyState size={ComponentSize.Small} className="alert-column--empty">
|
||||
<EmptyState.Text
|
||||
text="A Notification Endpoint stores the information to connect to a third party service that can receive notifications like Slack, PagerDuty, or an HTTP server"
|
||||
highlightWords={['Notification', 'Endpoint']}
|
||||
/>
|
||||
<br />
|
||||
<a href="#" target="_blank">
|
||||
Documentation
|
||||
</a>
|
||||
</EmptyState>
|
||||
<EndpointCards endpoints={endpoints} />
|
||||
</AlertsColumn>
|
||||
)
|
||||
}
|
||||
|
||||
export default EndpointsColumn
|
||||
const mstp = ({endpoints}: AppState) => {
|
||||
return {endpoints: endpoints.list}
|
||||
}
|
||||
|
||||
export default connect<StateProps>(mstp)(withRouter<OwnProps>(EndpointsColumn))
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/* eslint no-console: 0 */
|
||||
|
||||
// Libraries
|
||||
import React, {FC} from 'react'
|
||||
|
||||
// Components
|
||||
import {SlideToggle, ComponentSize, ResourceCard} from '@influxdata/clockface'
|
||||
import EndpointCardMenu from 'src/alerting/components/endpoints/EndpointCardMenu'
|
||||
|
||||
// Types
|
||||
import {NotificationEndpoint} from 'src/types'
|
||||
|
||||
interface OwnProps {
|
||||
endpoint: NotificationEndpoint
|
||||
}
|
||||
|
||||
type Props = OwnProps
|
||||
|
||||
const EndpointCard: FC<Props> = ({endpoint}) => {
|
||||
const {id, name, status} = endpoint
|
||||
|
||||
const handleUpdateName = () => console.trace('implement update endpoint name')
|
||||
const handleClick = () => console.trace('implement click endpoint name')
|
||||
|
||||
const nameComponent = (
|
||||
<ResourceCard.EditableName
|
||||
key={id}
|
||||
name={name}
|
||||
onClick={handleClick}
|
||||
onUpdate={handleUpdateName}
|
||||
testID="endpoint-card--name"
|
||||
inputTestID="endpoint-card--input"
|
||||
buttonTestID="endpoint-card--name-button"
|
||||
noNameString="Name this notification endpoint"
|
||||
/>
|
||||
)
|
||||
|
||||
const handleToggle = () => console.trace('implement toggle status')
|
||||
const toggle = (
|
||||
<SlideToggle
|
||||
active={status === 'active'}
|
||||
size={ComponentSize.ExtraSmall}
|
||||
onChange={handleToggle}
|
||||
testID="endpoint-card--slide-toggle"
|
||||
/>
|
||||
)
|
||||
|
||||
const handleDelete = () => console.trace('implement delete')
|
||||
const handleExport = () => console.trace('implement export')
|
||||
const handleClone = () => console.trace('implement delete')
|
||||
const contextMenu = (
|
||||
<EndpointCardMenu
|
||||
onDelete={handleDelete}
|
||||
onExport={handleExport}
|
||||
onClone={handleClone}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<ResourceCard
|
||||
key={id}
|
||||
toggle={toggle}
|
||||
name={nameComponent}
|
||||
contextMenu={contextMenu}
|
||||
disabled={status === 'inactive'}
|
||||
metaData={[<>{endpoint.updatedAt}</>]}
|
||||
testID={`endpoint-card ${name}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default EndpointCard
|
|
@ -0,0 +1,38 @@
|
|||
// Libraries
|
||||
import React, {FC} from 'react'
|
||||
|
||||
// Components
|
||||
import {Context, IconFont} from 'src/clockface'
|
||||
import {ComponentColor} from '@influxdata/clockface'
|
||||
|
||||
interface Props {
|
||||
onDelete: () => void
|
||||
onClone: () => void
|
||||
onExport: () => void
|
||||
}
|
||||
|
||||
const EndpointCardContext: FC<Props> = ({onDelete, onClone, onExport}) => {
|
||||
return (
|
||||
<Context>
|
||||
<Context.Menu icon={IconFont.CogThick}>
|
||||
<Context.Item label="Export" action={onExport} />
|
||||
</Context.Menu>
|
||||
<Context.Menu icon={IconFont.Duplicate} color={ComponentColor.Secondary}>
|
||||
<Context.Item label="Clone" action={onClone} />
|
||||
</Context.Menu>
|
||||
<Context.Menu
|
||||
icon={IconFont.Trash}
|
||||
color={ComponentColor.Danger}
|
||||
testID="context-delete-menu"
|
||||
>
|
||||
<Context.Item
|
||||
label="Delete"
|
||||
action={onDelete}
|
||||
testID="context-delete-task"
|
||||
/>
|
||||
</Context.Menu>
|
||||
</Context>
|
||||
)
|
||||
}
|
||||
|
||||
export default EndpointCardContext
|
|
@ -0,0 +1,42 @@
|
|||
// Libraries
|
||||
import React, {FC} from 'react'
|
||||
|
||||
// Components
|
||||
import EndpointCard from 'src/alerting/components/endpoints/EndpointCard'
|
||||
import {EmptyState, ResourceList, ComponentSize} from '@influxdata/clockface'
|
||||
|
||||
// Types
|
||||
import {NotificationEndpoint} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
endpoints: NotificationEndpoint[]
|
||||
}
|
||||
|
||||
const EndpointCards: FC<Props> = ({endpoints}) => {
|
||||
const cards = endpoints.map(endpoint => (
|
||||
<EndpointCard key={endpoint.id} endpoint={endpoint} />
|
||||
))
|
||||
|
||||
return (
|
||||
<ResourceList>
|
||||
<ResourceList.Body emptyState={<EmptyEndpointList />}>
|
||||
{cards}
|
||||
</ResourceList.Body>
|
||||
</ResourceList>
|
||||
)
|
||||
}
|
||||
|
||||
const EmptyEndpointList: FC = () => (
|
||||
<EmptyState size={ComponentSize.Small} className="alert-column--empty">
|
||||
<EmptyState.Text
|
||||
text="A Notification Endpoint stores the information to connect to a third party service that can receive notifications like Slack, PagerDuty, or an HTTP server"
|
||||
highlightWords={['Notification', 'Endpoint']}
|
||||
/>
|
||||
<br />
|
||||
<a href="#" target="_blank">
|
||||
Documentation
|
||||
</a>
|
||||
</EmptyState>
|
||||
)
|
||||
|
||||
export default EndpointCards
|
|
@ -0,0 +1,76 @@
|
|||
// Libraries
|
||||
import React, {FC, ChangeEvent} from 'react'
|
||||
|
||||
// Components
|
||||
import EndpointOptionsSlack from './EndpointOptionsSlack'
|
||||
import EndpointOptionsPagerDuty from './EndpointOptionsPagerDuty'
|
||||
import EndpointOptionsWebhook from './EndpointOptionsWebhook'
|
||||
|
||||
// Types
|
||||
import {
|
||||
NotificationEndpoint,
|
||||
SlackNotificationEndpoint,
|
||||
PagerDutyNotificationEndpoint,
|
||||
WebhookNotificationEndpoint,
|
||||
} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
endpoint: NotificationEndpoint
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
const EndpointOptions: FC<Props> = ({endpoint, onChange}) => {
|
||||
switch (endpoint.type) {
|
||||
case 'slack': {
|
||||
const {url, token} = endpoint as SlackNotificationEndpoint
|
||||
return (
|
||||
<EndpointOptionsSlack url={url} token={token} onChange={onChange} />
|
||||
)
|
||||
}
|
||||
case 'pagerduty': {
|
||||
const {url, routingKey} = endpoint as PagerDutyNotificationEndpoint
|
||||
return (
|
||||
<EndpointOptionsPagerDuty
|
||||
url={url}
|
||||
routingKey={routingKey}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'webhook': {
|
||||
// TODO(watts): add webhook type to the `Destination` dropdown
|
||||
// when webhooks are implemented in the backend.
|
||||
const {
|
||||
url,
|
||||
token,
|
||||
username,
|
||||
password,
|
||||
method,
|
||||
authmethod,
|
||||
contentTemplate,
|
||||
} = endpoint as WebhookNotificationEndpoint
|
||||
return (
|
||||
<EndpointOptionsWebhook
|
||||
url={url}
|
||||
token={token}
|
||||
username={username}
|
||||
password={password}
|
||||
method={method}
|
||||
authmethod={authmethod}
|
||||
contentTemplate={contentTemplate}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown endpoint type for endpoint: ${JSON.stringify(
|
||||
endpoint,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default EndpointOptions
|
|
@ -0,0 +1,36 @@
|
|||
// Libraries
|
||||
import React, {FC, ChangeEvent} from 'react'
|
||||
|
||||
// Components
|
||||
import {Input, FormElement} from '@influxdata/clockface'
|
||||
|
||||
interface Props {
|
||||
url: string
|
||||
routingKey: string
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
const EndpointOptionsPagerDuty: FC<Props> = ({url, routingKey, onChange}) => {
|
||||
return (
|
||||
<>
|
||||
<FormElement label="URL">
|
||||
<Input
|
||||
name="url"
|
||||
value={url}
|
||||
testID="pagerduty-url"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormElement>
|
||||
<FormElement label="Routing Key">
|
||||
<Input
|
||||
name="routingKey"
|
||||
value={routingKey}
|
||||
testID="pagerduty-routing-key"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormElement>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default EndpointOptionsPagerDuty
|
|
@ -0,0 +1,31 @@
|
|||
// Libraries
|
||||
import React, {FC, ChangeEvent} from 'react'
|
||||
|
||||
// Components
|
||||
import {Input, FormElement} from '@influxdata/clockface'
|
||||
|
||||
interface Props {
|
||||
url: string
|
||||
token: string
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
const EndpointOptionsSlack: FC<Props> = ({url, token, onChange}) => {
|
||||
return (
|
||||
<>
|
||||
<FormElement label="URL">
|
||||
<Input name="url" value={url} testID="slack-url" onChange={onChange} />
|
||||
</FormElement>
|
||||
<FormElement label="Token">
|
||||
<Input
|
||||
name="token"
|
||||
value={token}
|
||||
testID="slack-token"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormElement>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default EndpointOptionsSlack
|
|
@ -0,0 +1,43 @@
|
|||
// Libraries
|
||||
import React, {FC} from 'react'
|
||||
|
||||
// Components
|
||||
import {Input, FormElement} from '@influxdata/clockface'
|
||||
|
||||
// Types
|
||||
import {WebhookNotificationEndpoint} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
url: string
|
||||
token?: string
|
||||
username?: string
|
||||
password?: string
|
||||
method?: WebhookNotificationEndpoint['method']
|
||||
authmethod?: WebhookNotificationEndpoint['authmethod']
|
||||
contentTemplate: string
|
||||
}
|
||||
|
||||
const EndpointOptionsWebhook: FC<Props> = ({url, token}) => {
|
||||
return (
|
||||
<>
|
||||
<FormElement label="URL">
|
||||
<Input name="url" value={url} />
|
||||
</FormElement>
|
||||
<FormElement label="Token">
|
||||
<Input name="token" value={token} />
|
||||
</FormElement>
|
||||
<FormElement label="username">
|
||||
<Input name="username" value={token} />
|
||||
</FormElement>
|
||||
<FormElement label="password">
|
||||
<Input name="password" value={token} />
|
||||
</FormElement>
|
||||
{/** add dropdowns for method and authmethod */}
|
||||
<FormElement label="Content Template">
|
||||
<Input name="contentTemplate" value={token} />
|
||||
</FormElement>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default EndpointOptionsWebhook
|
|
@ -0,0 +1,29 @@
|
|||
// Types
|
||||
import {NotificationEndpoint} from 'src/types'
|
||||
|
||||
export type Action =
|
||||
| {type: 'UPDATE_ENDPOINT'; endpoint: NotificationEndpoint}
|
||||
| {type: 'DELETE_ENDPOINT'; endpointID: string}
|
||||
|
||||
export type EndpointState = NotificationEndpoint
|
||||
|
||||
export const reducer = (state: EndpointState, action: Action) => {
|
||||
switch (action.type) {
|
||||
case 'UPDATE_ENDPOINT': {
|
||||
const {endpoint} = action
|
||||
return {...state, ...endpoint}
|
||||
}
|
||||
case 'DELETE_ENDPOINT': {
|
||||
return state
|
||||
}
|
||||
|
||||
default:
|
||||
const neverAction: never = action
|
||||
|
||||
throw new Error(
|
||||
`Unhandled action "${
|
||||
(neverAction as any).type
|
||||
}" in EndpointsOverlay.reducer.ts`
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
.endpoint-overlay-footer--error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: $c-fire;
|
||||
font-size: $ix-text-base;
|
||||
font-weight: 600;
|
||||
margin: $ix-marg-c 0;
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
// Libraries
|
||||
import React, {FC, ChangeEvent} from 'react'
|
||||
|
||||
// Components
|
||||
import {Grid, Form, Panel, Input, TextArea} from '@influxdata/clockface'
|
||||
import EndpointOptions from 'src/alerting/components/endpoints/EndpointOptions'
|
||||
import EndpointTypeDropdown from 'src/alerting/components/endpoints/EndpointTypeDropdown'
|
||||
import EndpointOverlayFooter from 'src/alerting/components/endpoints/EndpointOverlayFooter'
|
||||
|
||||
// Hooks
|
||||
import {useEndpointReducer} from './EndpointOverlayProvider'
|
||||
|
||||
// Types
|
||||
import {NotificationEndpointType, NotificationEndpoint} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
onSave: (endpoint: NotificationEndpoint) => Promise<void>
|
||||
saveButtonText: string
|
||||
}
|
||||
|
||||
const EndpointOverlayContents: FC<Props> = ({onSave, saveButtonText}) => {
|
||||
const [endpoint, dispatch] = useEndpointReducer()
|
||||
const handleChange = (
|
||||
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
const {name, value} = e.target
|
||||
dispatch({
|
||||
type: 'UPDATE_ENDPOINT',
|
||||
endpoint: {...endpoint, [name]: value},
|
||||
})
|
||||
}
|
||||
|
||||
const handleSelectType = (type: NotificationEndpointType) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_ENDPOINT',
|
||||
endpoint: {...endpoint, type},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
<Form>
|
||||
<Grid.Row>
|
||||
<Grid.Column>
|
||||
<Panel>
|
||||
<Panel.Body>
|
||||
<Form.Element label="Name">
|
||||
<Input
|
||||
testID="endpoint-name--input"
|
||||
placeholder="Name this endpoint"
|
||||
value={endpoint.name}
|
||||
name="name"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Form.Element>
|
||||
<Form.Element label="Description">
|
||||
<TextArea
|
||||
className="endpoint-description--textarea"
|
||||
testID="endpoint-description--textarea"
|
||||
name="description"
|
||||
placeholder="Optional"
|
||||
value={endpoint.description}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Form.Element>
|
||||
<Form.Element label="Destination">
|
||||
<EndpointTypeDropdown
|
||||
onSelectType={handleSelectType}
|
||||
selectedType={endpoint.type}
|
||||
/>
|
||||
</Form.Element>
|
||||
<EndpointOptions endpoint={endpoint} onChange={handleChange} />
|
||||
<EndpointOverlayFooter
|
||||
onSave={onSave}
|
||||
saveButtonText={saveButtonText}
|
||||
/>
|
||||
</Panel.Body>
|
||||
</Panel>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Form>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default EndpointOverlayContents
|
|
@ -0,0 +1,74 @@
|
|||
// Libraries
|
||||
import React, {useState, FC} from 'react'
|
||||
|
||||
// Components
|
||||
import {
|
||||
Form,
|
||||
Grid,
|
||||
Button,
|
||||
Columns,
|
||||
ComponentColor,
|
||||
ComponentStatus,
|
||||
} from '@influxdata/clockface'
|
||||
|
||||
// Hooks
|
||||
import {useEndpointState} from './EndpointOverlayProvider'
|
||||
|
||||
// Types
|
||||
import {NotificationEndpoint, RemoteDataState} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
saveButtonText: string
|
||||
onSave: (endpoint: NotificationEndpoint) => Promise<void>
|
||||
}
|
||||
|
||||
const EndpointOverlayFooter: FC<Props> = ({saveButtonText, onSave}) => {
|
||||
const endpoint = useEndpointState()
|
||||
|
||||
const [saveStatus, setSaveStatus] = useState(RemoteDataState.NotStarted)
|
||||
const [errorMessage, setErrorMessage] = useState<string>(null)
|
||||
|
||||
const handleSave = async () => {
|
||||
if (saveStatus === RemoteDataState.Loading) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setSaveStatus(RemoteDataState.Loading)
|
||||
setErrorMessage(null)
|
||||
|
||||
await onSave(endpoint)
|
||||
} catch (e) {
|
||||
setSaveStatus(RemoteDataState.Error)
|
||||
setErrorMessage(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const buttonStatus =
|
||||
saveStatus === RemoteDataState.Loading
|
||||
? ComponentStatus.Loading
|
||||
: ComponentStatus.Default
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid.Row>
|
||||
<Grid.Column widthXS={Columns.Twelve}>
|
||||
{errorMessage && (
|
||||
<div className="endpoint-overlay-footer--error">{errorMessage}</div>
|
||||
)}
|
||||
<Form.Footer className="endpoint-overlay-footer">
|
||||
<Button
|
||||
testID="endpoint-save--button"
|
||||
onClick={handleSave}
|
||||
text={saveButtonText}
|
||||
status={buttonStatus}
|
||||
color={ComponentColor.Primary}
|
||||
/>
|
||||
</Form.Footer>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default EndpointOverlayFooter
|
|
@ -0,0 +1,55 @@
|
|||
// Libraries
|
||||
import React, {
|
||||
FC,
|
||||
Dispatch,
|
||||
createContext,
|
||||
useReducer,
|
||||
useRef,
|
||||
useContext,
|
||||
} from 'react'
|
||||
|
||||
// Reducer
|
||||
import {EndpointState, Action, reducer} from './EndpointOverlay.reducer'
|
||||
|
||||
const EndpointStateContext = createContext<EndpointState>(null)
|
||||
const EndpointDispatchContext = createContext<Dispatch<Action>>(null)
|
||||
|
||||
export const EndpointOverlayProvider: FC<{initialState: EndpointState}> = ({
|
||||
initialState,
|
||||
children,
|
||||
}) => {
|
||||
const prevInitialStateRef = useRef(initialState)
|
||||
|
||||
const [state, dispatch] = useReducer(
|
||||
(state: EndpointState, action: Action) => {
|
||||
if (prevInitialStateRef.current !== initialState) {
|
||||
prevInitialStateRef.current = initialState
|
||||
|
||||
return initialState
|
||||
}
|
||||
|
||||
return reducer(state, action)
|
||||
},
|
||||
initialState
|
||||
)
|
||||
|
||||
return (
|
||||
<EndpointStateContext.Provider value={state}>
|
||||
<EndpointDispatchContext.Provider value={dispatch}>
|
||||
{children}
|
||||
</EndpointDispatchContext.Provider>
|
||||
</EndpointStateContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useEndpointState = (): EndpointState => {
|
||||
return useContext(EndpointStateContext)
|
||||
}
|
||||
|
||||
export const useEndpointDispatch = (): Dispatch<Action> => {
|
||||
return useContext(EndpointDispatchContext)
|
||||
}
|
||||
|
||||
export const useEndpointReducer = (): [EndpointState, Dispatch<Action>] => {
|
||||
return [useEndpointState(), useEndpointDispatch()]
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
// Libraries
|
||||
import React, {FC} from 'react'
|
||||
|
||||
// Components
|
||||
import {Dropdown} from '@influxdata/clockface'
|
||||
|
||||
// Types
|
||||
import {NotificationEndpointType} from 'src/types'
|
||||
|
||||
interface EndpointType {
|
||||
id: NotificationEndpointType
|
||||
type: NotificationEndpointType
|
||||
name: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
selectedType: string
|
||||
onSelectType: (type: NotificationEndpointType) => void
|
||||
}
|
||||
|
||||
const types: EndpointType[] = [
|
||||
{name: 'Slack', type: 'slack', id: 'slack'},
|
||||
{name: 'Pagerduty', type: 'pagerduty', id: 'pagerduty'},
|
||||
]
|
||||
|
||||
const EndpointTypeDropdown: FC<Props> = ({selectedType, onSelectType}) => {
|
||||
const items = types.map(({id, type, name}) => (
|
||||
<Dropdown.Item
|
||||
key={id}
|
||||
id={id}
|
||||
value={id}
|
||||
testID={`endpoint--dropdown-item ${type}`}
|
||||
onClick={onSelectType}
|
||||
>
|
||||
{name}
|
||||
</Dropdown.Item>
|
||||
))
|
||||
|
||||
const selected = types.find(t => t.type === selectedType)
|
||||
|
||||
if (!selected) {
|
||||
throw new Error(
|
||||
'Incorrect endpoint type provided to <EndpointTypeDropdown/>'
|
||||
)
|
||||
}
|
||||
|
||||
const button = (active, onClick) => (
|
||||
<Dropdown.Button
|
||||
testID="endpoint--dropdown--button"
|
||||
active={active}
|
||||
onClick={onClick}
|
||||
>
|
||||
{selected.name}
|
||||
</Dropdown.Button>
|
||||
)
|
||||
|
||||
const menu = onCollapse => (
|
||||
<Dropdown.Menu onCollapse={onCollapse}>{items}</Dropdown.Menu>
|
||||
)
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
button={button}
|
||||
menu={menu}
|
||||
widthPixels={160}
|
||||
testID="endpoint-change--dropdown"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default EndpointTypeDropdown
|
|
@ -0,0 +1,64 @@
|
|||
// Libraries
|
||||
import React, {FC, useMemo} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {withRouter, WithRouterProps} from 'react-router'
|
||||
|
||||
// Actions
|
||||
import {createEndpoint} from 'src/alerting/actions/notifications/endpoints'
|
||||
|
||||
// Components
|
||||
import {Overlay} from '@influxdata/clockface'
|
||||
import {EndpointOverlayProvider} from 'src/alerting/components/endpoints/EndpointOverlayProvider'
|
||||
import EndpointOverlayContents from 'src/alerting/components/endpoints/EndpointOverlayContents'
|
||||
|
||||
// Constants
|
||||
import {NEW_ENDPOINT_DRAFT} from 'src/alerting/constants'
|
||||
import {NotificationEndpoint} from 'src/types'
|
||||
|
||||
interface DispatchProps {
|
||||
onCreateEndpoint: typeof createEndpoint
|
||||
}
|
||||
|
||||
type Props = WithRouterProps & DispatchProps
|
||||
|
||||
const NewRuleOverlay: FC<Props> = ({params, router, onCreateEndpoint}) => {
|
||||
const {orgID} = params
|
||||
const handleDismiss = () => {
|
||||
router.push(`/orgs/${params.orgID}/alerting`)
|
||||
}
|
||||
|
||||
const handleCreateEndpoint = async (endpoint: NotificationEndpoint) => {
|
||||
await onCreateEndpoint(endpoint)
|
||||
|
||||
handleDismiss()
|
||||
}
|
||||
|
||||
const initialState = useMemo(() => ({...NEW_ENDPOINT_DRAFT, orgID}), [orgID])
|
||||
|
||||
return (
|
||||
<EndpointOverlayProvider initialState={initialState}>
|
||||
<Overlay visible={true}>
|
||||
<Overlay.Container maxWidth={600}>
|
||||
<Overlay.Header
|
||||
title="Create a Notification Endpoint"
|
||||
onDismiss={handleDismiss}
|
||||
/>
|
||||
<Overlay.Body />
|
||||
<EndpointOverlayContents
|
||||
onSave={handleCreateEndpoint}
|
||||
saveButtonText="Create Notification Endpoint"
|
||||
/>
|
||||
</Overlay.Container>
|
||||
</Overlay>
|
||||
</EndpointOverlayProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const mdtp = {
|
||||
onCreateEndpoint: createEndpoint,
|
||||
}
|
||||
|
||||
export default connect<null, DispatchProps>(
|
||||
null,
|
||||
mdtp
|
||||
)(withRouter<Props>(NewRuleOverlay))
|
|
@ -1,5 +1,5 @@
|
|||
// Libraries
|
||||
import React, {FunctionComponent} from 'react'
|
||||
import React, {FC} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {withRouter, WithRouterProps} from 'react-router'
|
||||
|
||||
|
@ -27,7 +27,7 @@ interface OwnProps {
|
|||
|
||||
type Props = OwnProps & DispatchProps & WithRouterProps
|
||||
|
||||
const RuleCard: FunctionComponent<Props> = ({
|
||||
const RuleCard: FC<Props> = ({
|
||||
rule,
|
||||
updateRule,
|
||||
deleteNotificationRule,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Libraries
|
||||
import React, {FunctionComponent} from 'react'
|
||||
import React, {FC} from 'react'
|
||||
|
||||
// Components
|
||||
import NotificationRuleCard from 'src/alerting/components/notifications/RuleCard'
|
||||
|
@ -13,7 +13,7 @@ interface Props {
|
|||
rules: NotificationRuleDraft[]
|
||||
}
|
||||
|
||||
const NotificationRuleCards: FunctionComponent<Props> = ({rules}) => {
|
||||
const NotificationRuleCards: FC<Props> = ({rules}) => {
|
||||
return (
|
||||
<ResourceList>
|
||||
<ResourceList.Body emptyState={<EmptyNotificationRulesList />}>
|
||||
|
@ -25,7 +25,7 @@ const NotificationRuleCards: FunctionComponent<Props> = ({rules}) => {
|
|||
)
|
||||
}
|
||||
|
||||
const EmptyNotificationRulesList: FunctionComponent = () => {
|
||||
const EmptyNotificationRulesList: FC = () => {
|
||||
return (
|
||||
<EmptyState size={ComponentSize.Small} className="alert-column--empty">
|
||||
<EmptyState.Text
|
||||
|
|
|
@ -65,7 +65,7 @@ const RuleEndpointDropdown: FC<Props> = ({
|
|||
button={button}
|
||||
menu={menu}
|
||||
widthPixels={160}
|
||||
testID="status-change--dropdown"
|
||||
testID="endpoint-change--dropdown"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -33,10 +33,6 @@ export const getRuleVariantDefaults = (
|
|||
return {messageTemplate: '', channel: '', type: 'slack'}
|
||||
}
|
||||
|
||||
case 'smtp': {
|
||||
return {to: '', bodyTemplate: '', subjectTemplate: '', type: 'smtp'}
|
||||
}
|
||||
|
||||
case 'pagerduty': {
|
||||
return {messageTemplate: '', type: 'pagerduty'}
|
||||
}
|
||||
|
|
|
@ -103,6 +103,17 @@ export const NEW_TAG_RULE_DRAFT: TagRuleDraft = {
|
|||
},
|
||||
}
|
||||
|
||||
export const NEW_ENDPOINT_DRAFT: NotificationEndpoint = {
|
||||
orgID: '1',
|
||||
userID: '1',
|
||||
description: 'interrupt everyone at work',
|
||||
name: 'Slack',
|
||||
status: 'active',
|
||||
type: 'slack',
|
||||
token: 'plerpstokeny',
|
||||
url: 'insert.slack.url.here',
|
||||
}
|
||||
|
||||
export const NEW_ENDPOINT_FIXTURES: NotificationEndpoint[] = [
|
||||
{
|
||||
id: '1',
|
||||
|
@ -112,15 +123,8 @@ export const NEW_ENDPOINT_FIXTURES: NotificationEndpoint[] = [
|
|||
name: 'Slack',
|
||||
status: 'active',
|
||||
type: 'slack',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
orgID: '1',
|
||||
userID: '1',
|
||||
description: 'interrupt someone by email',
|
||||
name: 'SMTP',
|
||||
status: 'active',
|
||||
type: 'smtp',
|
||||
url: 'insert.slack.url.here',
|
||||
token: 'plerps',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
|
@ -130,5 +134,7 @@ export const NEW_ENDPOINT_FIXTURES: NotificationEndpoint[] = [
|
|||
name: 'PagerDuty',
|
||||
status: 'active',
|
||||
type: 'pagerduty',
|
||||
url: 'insert.pagerduty.url.here',
|
||||
routingKey: 'plerpsy',
|
||||
},
|
||||
]
|
||||
|
|
|
@ -35,7 +35,9 @@ const AlertingIndex: FunctionComponent = ({children}) => {
|
|||
</GetResources>
|
||||
</GridColumn>
|
||||
<GridColumn widthLG={4} widthMD={4} widthSM={4} widthXS={12}>
|
||||
<EndpointsColumn />
|
||||
<GetResources resource={ResourceTypes.NotificationEndpoints}>
|
||||
<EndpointsColumn />
|
||||
</GetResources>
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
</Grid>
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
// Libraries
|
||||
import produce from 'immer'
|
||||
|
||||
// Types
|
||||
import {NotificationEndpoint} from 'src/types'
|
||||
import {RemoteDataState} from '@influxdata/clockface'
|
||||
import {Action} from 'src/alerting/actions/notifications/endpoints'
|
||||
|
||||
export interface NotificationEndpointsState {
|
||||
status: RemoteDataState
|
||||
list: NotificationEndpoint[]
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
status: RemoteDataState.NotStarted,
|
||||
list: [],
|
||||
}
|
||||
|
||||
type State = NotificationEndpointsState
|
||||
|
||||
export default (
|
||||
state: State = initialState,
|
||||
action: Action
|
||||
): NotificationEndpointsState =>
|
||||
produce(state, draftState => {
|
||||
switch (action.type) {
|
||||
case 'SET_ALL_ENDPOINTS': {
|
||||
const {status, endpoints} = action
|
||||
|
||||
if (endpoints) {
|
||||
draftState.list = endpoints
|
||||
}
|
||||
|
||||
draftState.status = status
|
||||
|
||||
return
|
||||
}
|
||||
case 'SET_ENDPOINT': {
|
||||
const {endpoint} = action
|
||||
const index = state.list.findIndex(ep => ep.id === endpoint.id)
|
||||
|
||||
if (index === -1) {
|
||||
draftState.list.push(endpoint)
|
||||
return
|
||||
}
|
||||
|
||||
draftState.list[index] = endpoint
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
|
@ -83,6 +83,7 @@ import NewCheckEO from 'src/alerting/components/NewCheckEO'
|
|||
import EditCheckEO from 'src/alerting/components/EditCheckEO'
|
||||
import NewRuleOverlay from 'src/alerting/components/notifications/NewRuleOverlay'
|
||||
import EditRuleOverlay from 'src/alerting/components/notifications/EditRuleOverlay'
|
||||
import NewEndpointOverlay from 'src/alerting/components/endpoints/NewEndpointOverlay'
|
||||
|
||||
import {FeatureFlag} from 'src/shared/utils/featureFlag'
|
||||
|
||||
|
@ -332,6 +333,14 @@ class Root extends PureComponent {
|
|||
path="rules/:ruleID/edit"
|
||||
component={EditRuleOverlay}
|
||||
/>
|
||||
<Route
|
||||
path="endpoints/new"
|
||||
component={NewEndpointOverlay}
|
||||
/>
|
||||
<Route
|
||||
path="rules/:ruleID/edit"
|
||||
component={null}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path="alert-history"
|
||||
|
|
|
@ -16,6 +16,7 @@ import {getTemplates} from 'src/templates/actions'
|
|||
import {getMembers, getUsers} from 'src/members/actions'
|
||||
import {getChecks} from 'src/alerting/actions/checks'
|
||||
import {getNotificationRules} from 'src/alerting/actions/notifications/rules'
|
||||
import {getEndpoints} from 'src/alerting/actions/notifications/endpoints'
|
||||
|
||||
// Types
|
||||
import {AppState} from 'src/types'
|
||||
|
@ -31,6 +32,7 @@ import {TemplatesState} from 'src/templates/reducers'
|
|||
import {MembersState, UsersMap} from 'src/members/reducers'
|
||||
import {ChecksState} from 'src/alerting/reducers/checks'
|
||||
import {NotificationRulesState} from 'src/alerting/reducers/notifications/rules'
|
||||
import {NotificationEndpointsState} from 'src/alerting/reducers/notifications/endpoints'
|
||||
|
||||
// Components
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
@ -54,6 +56,7 @@ interface StateProps {
|
|||
users: {status: RemoteDataState; item: UsersMap}
|
||||
checks: ChecksState
|
||||
rules: NotificationRulesState
|
||||
endpoints: NotificationEndpointsState
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
|
@ -70,6 +73,7 @@ interface DispatchProps {
|
|||
getUsers: typeof getUsers
|
||||
getChecks: typeof getChecks
|
||||
getNotificationRules: typeof getNotificationRules
|
||||
getEndpoints: typeof getEndpoints
|
||||
}
|
||||
|
||||
interface PassedProps {
|
||||
|
@ -92,6 +96,7 @@ export enum ResourceTypes {
|
|||
Users = 'users',
|
||||
Checks = 'checks',
|
||||
NotificationRules = 'rules',
|
||||
NotificationEndpoints = 'endpoints',
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
|
@ -150,6 +155,10 @@ class GetResources extends PureComponent<Props, StateProps> {
|
|||
return await this.props.getNotificationRules()
|
||||
}
|
||||
|
||||
case ResourceTypes.NotificationEndpoints: {
|
||||
return await this.props.getEndpoints()
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error('incorrect resource type provided')
|
||||
}
|
||||
|
@ -183,6 +192,7 @@ const mstp = ({
|
|||
members,
|
||||
checks,
|
||||
rules,
|
||||
endpoints,
|
||||
}: AppState): StateProps => {
|
||||
return {
|
||||
labels,
|
||||
|
@ -198,6 +208,7 @@ const mstp = ({
|
|||
users: members.users,
|
||||
checks,
|
||||
rules,
|
||||
endpoints,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -215,6 +226,7 @@ const mdtp = {
|
|||
getUsers: getUsers,
|
||||
getChecks: getChecks,
|
||||
getNotificationRules: getNotificationRules,
|
||||
getEndpoints: getEndpoints,
|
||||
}
|
||||
|
||||
export default connect<StateProps, DispatchProps, {}>(
|
||||
|
|
|
@ -190,3 +190,7 @@ $notification-margin: 12px;
|
|||
$ix-link-default-hover
|
||||
);
|
||||
}
|
||||
|
||||
.endpoint-description--textarea {
|
||||
max-height: 150;
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ import {autoRefreshReducer} from 'src/shared/reducers/autoRefresh'
|
|||
import {limitsReducer, LimitsState} from 'src/cloud/reducers/limits'
|
||||
import checksReducer from 'src/alerting/reducers/checks'
|
||||
import rulesReducer from 'src/alerting/reducers/notifications/rules'
|
||||
import endpointsReducer from 'src/alerting/reducers/notifications/endpoints'
|
||||
|
||||
// Types
|
||||
import {LocalStorage} from 'src/types/localStorage'
|
||||
|
@ -66,6 +67,7 @@ export const rootReducer = combineReducers<ReducerState>({
|
|||
cloud: combineReducers<{limits: LimitsState}>({limits: limitsReducer}),
|
||||
checks: checksReducer,
|
||||
rules: rulesReducer,
|
||||
endpoints: endpointsReducer,
|
||||
VERSION: () => '',
|
||||
})
|
||||
|
||||
|
|
|
@ -124,6 +124,7 @@
|
|||
@import 'src/timeMachine/components/AddCheckDialog.scss';
|
||||
@import 'src/alerting/components/SentTableField.scss';
|
||||
@import 'src/alerting/components/notifications/RuleOverlayFooter.scss';
|
||||
@import 'src/alerting/components/endpoints/EndpointOverlay.scss';
|
||||
|
||||
// External
|
||||
@import '../../node_modules/@influxdata/react-custom-scrollbars/dist/styles.css';
|
||||
|
|
|
@ -61,12 +61,16 @@ export {
|
|||
NotificationEndpoint,
|
||||
NotificationRuleBase,
|
||||
NotificationRule,
|
||||
NotificationEndpointType,
|
||||
SMTPNotificationRuleBase,
|
||||
SlackNotificationRuleBase,
|
||||
PagerDutyNotificationRuleBase,
|
||||
SMTPNotificationRule,
|
||||
SlackNotificationRule,
|
||||
PagerDutyNotificationRule,
|
||||
PagerDutyNotificationEndpoint,
|
||||
SlackNotificationEndpoint,
|
||||
WebhookNotificationEndpoint,
|
||||
} from '../client'
|
||||
|
||||
import {Check, Threshold} from '../client'
|
||||
|
|
|
@ -26,6 +26,7 @@ import {AutoRefreshState} from 'src/shared/reducers/autoRefresh'
|
|||
import {LimitsState} from 'src/cloud/reducers/limits'
|
||||
import {ChecksState} from 'src/alerting/reducers/checks'
|
||||
import {NotificationRulesState} from 'src/alerting/reducers/notifications/rules'
|
||||
import {NotificationEndpointsState} from 'src/alerting/reducers/notifications/endpoints'
|
||||
|
||||
export interface AppState {
|
||||
VERSION: string
|
||||
|
@ -57,6 +58,7 @@ export interface AppState {
|
|||
cloud: {limits: LimitsState}
|
||||
checks: ChecksState
|
||||
rules: NotificationRulesState
|
||||
endpoints: NotificationEndpointsState
|
||||
}
|
||||
|
||||
export type GetState = () => AppState
|
||||
|
|
Loading…
Reference in New Issue