diff --git a/CHANGELOG.md b/CHANGELOG.md index 5377ca40e93..c7305b0a090 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ 1. [16323](https://github.com/influxdata/influxdb/pull/16323): Add support for tasks to pkger apply functionality 1. [16324](https://github.com/influxdata/influxdb/pull/16324): Add support for tasks to pkger export functionality 1. [16226](https://github.com/influxdata/influxdb/pull/16226): Add group() to Query Builder +1. [16338](https://github.com/influxdata/influxdb/pull/16338): Add last run status to check and notification rules ### Bug Fixes diff --git a/http/check_service.go b/http/check_service.go index f3ac3c61844..90092e78696 100644 --- a/http/check_service.go +++ b/http/check_service.go @@ -6,6 +6,7 @@ import ( "fmt" "io/ioutil" "net/http" + "time" "github.com/influxdata/httprouter" "github.com/influxdata/influxdb" @@ -138,9 +139,13 @@ type checkLinks struct { type checkResponse struct { influxdb.Check - Status string `json:"status"` - Labels []influxdb.Label `json:"labels"` - Links checkLinks `json:"links"` + Status string `json:"status"` + Labels []influxdb.Label `json:"labels"` + Links checkLinks `json:"links"` + LatestCompleted time.Time `json:"latestCompleted,omitempty"` + LatestScheduled time.Time `json:"latestScheduled,omitempty"` + LastRunStatus string `json:"LastRunStatus,omitempty"` + LastRunError string `json:"LastRunError,omitempty"` } type postCheckRequest struct { @@ -159,13 +164,21 @@ func (resp checkResponse) MarshalJSON() ([]byte, error) { } b2, err := json.Marshal(struct { - Labels []influxdb.Label `json:"labels"` - Links checkLinks `json:"links"` - Status string `json:"status"` + Labels []influxdb.Label `json:"labels"` + Links checkLinks `json:"links"` + Status string `json:"status"` + LatestCompleted time.Time `json:"latestCompleted,omitempty"` + LatestScheduled time.Time `json:"latestScheduled,omitempty"` + LastRunStatus string `json:"lastRunStatus,omitempty"` + LastRunError string `json:"lastRunError,omitempty"` }{ - Links: resp.Links, - Labels: resp.Labels, - Status: resp.Status, + Links: resp.Links, + Labels: resp.Labels, + Status: resp.Status, + LatestCompleted: resp.LatestCompleted, + LatestScheduled: resp.LatestScheduled, + LastRunStatus: resp.LastRunStatus, + LastRunError: resp.LastRunError, }) if err != nil { return nil, err @@ -198,7 +211,11 @@ func (h *CheckHandler) newCheckResponse(ctx context.Context, chk influxdb.Check, Owners: fmt.Sprintf("/api/v2/checks/%s/owners", chk.GetID()), Query: fmt.Sprintf("/api/v2/checks/%s/query", chk.GetID()), }, - Labels: []influxdb.Label{}, + Labels: []influxdb.Label{}, + LatestCompleted: task.LatestCompleted, + LatestScheduled: task.LatestScheduled, + LastRunStatus: task.LastRunStatus, + LastRunError: task.LastRunError, } for _, l := range labels { diff --git a/http/check_test.go b/http/check_test.go index 35f5210be97..3461261f45b 100644 --- a/http/check_test.go +++ b/http/check_test.go @@ -168,7 +168,9 @@ func TestService_handleGetChecks(t *testing.T) { } } ], - "status": "active" + "status": "active", + "latestCompleted": "0001-01-01T00:00:00Z", + "latestScheduled": "0001-01-01T00:00:00Z" }, { "links": { @@ -230,7 +232,9 @@ func TestService_handleGetChecks(t *testing.T) { } } ], - "status": "active" + "status": "active", + "latestCompleted": "0001-01-01T00:00:00Z", + "latestScheduled": "0001-01-01T00:00:00Z" } ] } @@ -520,7 +524,9 @@ func TestService_handleGetCheck(t *testing.T) { "type": "deadman", "orgID": "020f755c3c082000", "name": "hello", - "status": "active" + "status": "active", + "latestCompleted": "0001-01-01T00:00:00Z", + "latestScheduled": "0001-01-01T00:00:00Z" } `, }, @@ -703,7 +709,9 @@ func TestService_handlePostCheck(t *testing.T) { "every": "5m", "level": "WARN", "labels": [], - "status": "active" + "status": "active", + "latestCompleted": "0001-01-01T00:00:00Z", + "latestScheduled": "0001-01-01T00:00:00Z" } `, }, @@ -949,7 +957,9 @@ func TestService_handlePatchCheck(t *testing.T) { "statusMessageTemplate": "", "tags": null, "type": "deadman", - "labels": [] + "labels": [], + "latestCompleted": "0001-01-01T00:00:00Z", + "latestScheduled": "0001-01-01T00:00:00Z" } `, }, @@ -1130,7 +1140,9 @@ func TestService_handleUpdateCheck(t *testing.T) { "statusMessageTemplate": "", "tags": null, "type": "deadman", - "labels": [] + "labels": [], + "latestCompleted": "0001-01-01T00:00:00Z", + "latestScheduled": "0001-01-01T00:00:00Z" } `, }, diff --git a/http/notification_rule.go b/http/notification_rule.go index 532025b2557..d4db95087ce 100644 --- a/http/notification_rule.go +++ b/http/notification_rule.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "net/http" + "time" "github.com/influxdata/httprouter" "github.com/influxdata/influxdb" @@ -146,9 +147,13 @@ type notificationRuleLinks struct { type notificationRuleResponse struct { influxdb.NotificationRule - Labels []influxdb.Label `json:"labels"` - Links notificationRuleLinks `json:"links"` - Status string `json:"status"` + Labels []influxdb.Label `json:"labels"` + Links notificationRuleLinks `json:"links"` + Status string `json:"status"` + LatestCompleted time.Time `json:"latestCompleted,omitempty"` + LatestScheduled time.Time `json:"latestScheduled,omitempty"` + LastRunStatus string `json:"LastRunStatus,omitempty"` + LastRunError string `json:"LastRunError,omitempty"` } func (resp notificationRuleResponse) MarshalJSON() ([]byte, error) { @@ -158,13 +163,21 @@ func (resp notificationRuleResponse) MarshalJSON() ([]byte, error) { } b2, err := json.Marshal(struct { - Labels []influxdb.Label `json:"labels"` - Links notificationRuleLinks `json:"links"` - Status string `json:"status"` + Labels []influxdb.Label `json:"labels"` + Links notificationRuleLinks `json:"links"` + Status string `json:"status"` + LatestCompleted time.Time `json:"latestCompleted,omitempty"` + LatestScheduled time.Time `json:"latestScheduled,omitempty"` + LastRunStatus string `json:"lastRunStatus,omitempty"` + LastRunError string `json:"lastRunError,omitempty"` }{ - Links: resp.Links, - Labels: resp.Labels, - Status: resp.Status, + Links: resp.Links, + Labels: resp.Labels, + Status: resp.Status, + LatestCompleted: resp.LatestCompleted, + LatestScheduled: resp.LatestScheduled, + LastRunStatus: resp.LastRunStatus, + LastRunError: resp.LastRunError, }) if err != nil { return nil, err @@ -195,8 +208,12 @@ func (h *NotificationRuleHandler) newNotificationRuleResponse(ctx context.Contex Owners: fmt.Sprintf("/api/v2/notificationRules/%s/owners", nr.GetID()), Query: fmt.Sprintf("/api/v2/notificationRules/%s/query", nr.GetID()), }, - Labels: []influxdb.Label{}, - Status: t.Status, + Labels: []influxdb.Label{}, + Status: t.Status, + LatestCompleted: t.LatestCompleted, + LatestScheduled: t.LatestScheduled, + LastRunStatus: t.LastRunStatus, + LastRunError: t.LastRunError, } for _, l := range labels { diff --git a/http/notification_rule_test.go b/http/notification_rule_test.go index edd64bee49f..305ef207ce7 100644 --- a/http/notification_rule_test.go +++ b/http/notification_rule_test.go @@ -147,7 +147,9 @@ func Test_newNotificationRuleResponses(t *testing.T) { ], "type": "slack", "updatedAt": "0001-01-01T00:00:00Z", - "status": "active" + "status": "active", + "latestCompleted": "0001-01-01T00:00:00Z", + "latestScheduled": "0001-01-01T00:00:00Z" }, { "createdAt": "0001-01-01T00:00:00Z", @@ -170,7 +172,9 @@ func Test_newNotificationRuleResponses(t *testing.T) { "runbookLink": "", "type": "pagerduty", "updatedAt": "0001-01-01T00:00:00Z", - "status": "active" + "status": "active", + "latestCompleted": "0001-01-01T00:00:00Z", + "latestScheduled": "0001-01-01T00:00:00Z" } ] }`, @@ -287,7 +291,9 @@ func Test_newNotificationRuleResponse(t *testing.T) { } ], "type": "slack", - "updatedAt": "0001-01-01T00:00:00Z" + "updatedAt": "0001-01-01T00:00:00Z", + "latestCompleted": "0001-01-01T00:00:00Z", + "latestScheduled": "0001-01-01T00:00:00Z" }`, }, } diff --git a/http/swagger.yml b/http/swagger.yml index 1aae905a814..afe52701dcb 100644 --- a/http/swagger.yml +++ b/http/swagger.yml @@ -10214,6 +10214,21 @@ components: statusMessageTemplate: description: The template used to generate and write a status message. type: string + latestCompleted: + description: Timestamp of latest scheduled, completed run, RFC3339. + type: string + format: date-time + readOnly: true + lastRunStatus: + readOnly: true + type: string + enum: + - failed + - success + - canceled + lastRunError: + readOnly: true + type: string labels: $ref: "#/components/schemas/Labels" links: @@ -10390,6 +10405,21 @@ components: - statusRules - endpointID properties: + latestCompleted: + description: Timestamp of latest scheduled, completed run, RFC3339. + type: string + format: date-time + readOnly: true + lastRunStatus: + readOnly: true + type: string + enum: + - failed + - success + - canceled + lastRunError: + readOnly: true + type: string id: readOnly: true type: string diff --git a/ui/cypress/e2e/checks.test.ts b/ui/cypress/e2e/checks.test.ts index 9b9e52b2a58..5974b7288fa 100644 --- a/ui/cypress/e2e/checks.test.ts +++ b/ui/cypress/e2e/checks.test.ts @@ -75,6 +75,7 @@ describe('Checks', () => { cy.getByTestID('add-threshold-condition-WARN').click() cy.getByTestID('save-cell--button').click() cy.getByTestID('check-card').should('have.length', 1) + cy.getByTestID('notification-error').should('not.exist') }) it('should allow created checks to be selected and routed to the edit page', () => { @@ -95,5 +96,23 @@ describe('Checks', () => { }) }) }) + + it('can toggle a check to on / off', () => { + cy.get('.cf-resource-card__disabled').should('not.exist') + cy.getByTestID('check-card--slide-toggle').click() + cy.getByTestID('notification-error').should('not.exist') + cy.get('.cf-resource-card__disabled').should('exist') + cy.getByTestID('check-card--slide-toggle').click() + cy.getByTestID('notification-error').should('not.exist') + cy.get('.cf-resource-card__disabled').should('not.exist') + }) + + it('can display the last run status', () => { + cy.getByTestID('last-run-status--icon').should('exist') + cy.getByTestID('last-run-status--icon').trigger('mouseover') + cy.getByTestID('popover--dialog') + .should('exist') + .contains('Last Run Status:') + }) }) }) diff --git a/ui/src/alerting/actions/checks.ts b/ui/src/alerting/actions/checks.ts index 2fd8d2f581c..6181ff440a2 100644 --- a/ui/src/alerting/actions/checks.ts +++ b/ui/src/alerting/actions/checks.ts @@ -182,6 +182,18 @@ export const saveCheckFromTimeMachine = () => async ( } } +export const updateCheck = (check: Partial) => async ( + dispatch: Dispatch +) => { + const resp = await api.putCheck({checkID: check.id, data: check as Check}) + if (resp.status === 200) { + dispatch(setCheck(resp.data)) + } else { + throw new Error(resp.data.message) + } + dispatch(setCheck(resp.data)) +} + const updateCheckFromTimeMachine = async (check: Check) => { // todo: refactor after https://github.com/influxdata/influxdb/issues/16317 const getCheckResponse = await api.getCheck({checkID: check.id}) diff --git a/ui/src/alerting/components/CheckCard.tsx b/ui/src/alerting/components/CheckCard.tsx index 326f0798d1f..66255055208 100644 --- a/ui/src/alerting/components/CheckCard.tsx +++ b/ui/src/alerting/components/CheckCard.tsx @@ -7,6 +7,7 @@ import {withRouter, WithRouterProps} from 'react-router' import {SlideToggle, ComponentSize, ResourceCard} from '@influxdata/clockface' import CheckCardContext from 'src/alerting/components/CheckCardContext' import InlineLabels from 'src/shared/components/inlineLabels/InlineLabels' +import LastRunTaskStatus from 'src/shared/components/lastRunTaskStatus/LastRunTaskStatus' // Constants import {DEFAULT_CHECK_NAME} from 'src/alerting/constants' @@ -170,7 +171,13 @@ const CheckCard: FunctionComponent = ({ /> } metaData={[ + <>Last completed at {check.latestCompleted}, <>{relativeTimestampFormatter(check.updatedAt, 'Last updated ')}, + , ]} /> ) diff --git a/ui/src/alerting/components/notifications/RuleCard.tsx b/ui/src/alerting/components/notifications/RuleCard.tsx index 9710a86ad8c..995a5b5d874 100644 --- a/ui/src/alerting/components/notifications/RuleCard.tsx +++ b/ui/src/alerting/components/notifications/RuleCard.tsx @@ -7,6 +7,7 @@ import {withRouter, WithRouterProps} from 'react-router' import {SlideToggle, ComponentSize, ResourceCard} from '@influxdata/clockface' import NotificationRuleCardContext from 'src/alerting/components/notifications/RuleCardContext' import InlineLabels from 'src/shared/components/inlineLabels/InlineLabels' +import LastRunTaskStatus from 'src/shared/components/lastRunTaskStatus/LastRunTaskStatus' // Constants import {DEFAULT_NOTIFICATION_RULE_NAME} from 'src/alerting/constants' @@ -165,7 +166,13 @@ const RuleCard: FC = ({ /> } metaData={[ + <>Last completed at {rule.latestCompleted}, <>{relativeTimestampFormatter(rule.updatedAt, 'Last updated ')}, + , ]} /> ) diff --git a/ui/src/shared/components/lastRunTaskStatus/LastRunTaskStatus.scss b/ui/src/shared/components/lastRunTaskStatus/LastRunTaskStatus.scss new file mode 100644 index 00000000000..dffe06e8fbd --- /dev/null +++ b/ui/src/shared/components/lastRunTaskStatus/LastRunTaskStatus.scss @@ -0,0 +1,70 @@ +@import '~src/style/_influx-colors.scss'; +@import '~src/style/_variables.scss'; + +.last-run-task-status { + width: 30px; + height: 30px; + font-size: 16px; + display: flex; + align-items: center; + justify-content: center; + align-content: center; + background-color: rgba($g5-pepper, 0.5); + border-radius: 50%; + margin-top: $ix-marg-a; + transition: color 0.25s ease, text-shadow 0.25s ease; + + &.last-run-task-status__danger { + color: $c-curacao; + + .cf-resource-card__disabled & { + color: mix($c-curacao, $g8-storm, 60%); + } + + &.last-run-task-status__highlight { + color: $c-dreamsicle; + text-shadow: 0 0 4px $c-fire; + } + } + + &.last-run-task-status__success { + color: $c-rainforest; + + .cf-resource-card__disabled & { + color: mix($c-rainforest, $g8-storm, 60%); + } + + &.last-run-task-status__highlight { + color: $c-honeydew; + text-shadow: 0 0 4px $c-viridian; + } + } + + &.last-run-task-status__warning { + color: $c-pineapple; + + .cf-resource-card__disabled & { + color: mix($c-pineapple, $g8-storm, 60%); + } + + &.last-run-task-status__highlight { + color: $c-thunder; + text-shadow: 0 0 4px $c-topaz; + } + } +} + +.last-run-task-status--popover .cf-popover--contents { + max-width: 300px; + padding: $ix-marg-b $ix-marg-b + $ix-marg-a; + font-size: 13px; + font-family: $code-font; + + > h6 { + margin-top: 0; + } + + > p { + margin-bottom: 0; + } +} diff --git a/ui/src/shared/components/lastRunTaskStatus/LastRunTaskStatus.tsx b/ui/src/shared/components/lastRunTaskStatus/LastRunTaskStatus.tsx new file mode 100644 index 00000000000..2cae4fe48d4 --- /dev/null +++ b/ui/src/shared/components/lastRunTaskStatus/LastRunTaskStatus.tsx @@ -0,0 +1,80 @@ +// Libraries +import React, {FC, useRef, useState} from 'react' +import classnames from 'classnames' + +// Components +import { + Icon, + IconFont, + Popover, + Appearance, + PopoverInteraction, + ComponentColor, +} from '@influxdata/clockface' + +// Styles +import './LastRunTaskStatus.scss' + +interface PassedProps { + lastRunError?: string + lastRunStatus: string +} + +const LastRunTaskStatus: FC = ({lastRunError, lastRunStatus}) => { + const triggerRef = useRef(null) + const [highlight, setHighlight] = useState(false) + + let color = ComponentColor.Success + let icon = IconFont.Checkmark + let text = 'Task ran successfully!' + + if (lastRunStatus === 'failed' || lastRunError !== undefined) { + color = ComponentColor.Danger + icon = IconFont.AlertTriangle + text = lastRunError + } + + if (lastRunStatus === 'cancel') { + color = ComponentColor.Warning + icon = IconFont.Remove + text = 'Task Cancelled' + } + + const statusClassName = classnames('last-run-task-status', { + [`last-run-task-status__${color}`]: color, + 'last-run-task-status__highlight': highlight, + }) + + const popoverContents = () => ( + <> +
Last Run Status:
+

{text}

+ + ) + + return ( + <> +
+ +
+ setHighlight(true)} + onHide={() => setHighlight(false)} + /> + + ) +} + +export default LastRunTaskStatus