From 15f689842675073a8ede14446c7162d0a502a4ad Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 13 Mar 2023 15:02:29 +0200 Subject: [PATCH] Rares/score quality (#1324) # What this PR does #118 ## Checklist - [x] Tests updated - [ ] Documentation added - [x] `CHANGELOG.md` updated --- CHANGELOG.md | 1 + .../ScheduleCounter.module.css | 13 +- .../ScheduleCounter/ScheduleCounter.tsx | 16 +- .../ScheduleQuality.module.css | 46 -- .../ScheduleQuality.module.scss | 39 ++ .../ScheduleQuality/ScheduleQuality.tsx | 187 +++++--- .../ScheduleQualityDetails.module.scss | 61 +++ .../ScheduleQualityDetails.tsx | 164 +++++++ .../ScheduleQualityProgressBar.module.scss | 43 ++ .../ScheduleQualityProgressBar.test.tsx | 58 +++ .../ScheduleQualityProgressBar.tsx | 87 ++++ .../ScheduleQualityProgressBar.test.tsx.snap | 449 ++++++++++++++++++ .../src/components/Tag/Tag.module.css | 2 +- grafana-plugin/src/components/Tag/Tag.tsx | 14 +- .../src/components/Text/Text.module.scss | 4 + grafana-plugin/src/components/Text/Text.tsx | 2 +- .../src/containers/AlertRules/parts/index.tsx | 3 +- .../EscalationChainSteps.tsx | 6 +- .../RotationForm/ScheduleOverrideForm.tsx | 4 +- .../src/models/schedule/schedule.ts | 6 + .../src/models/schedule/schedule.types.ts | 14 + .../src/pages/incident/Incident.helpers.tsx | 9 +- .../incidents/parts/IncidentDropdown.tsx | 9 +- .../src/pages/schedule/Schedule.module.css | 9 + .../src/pages/schedule/Schedule.tsx | 17 +- .../src/pages/schedules/Schedules.tsx | 30 +- grafana-plugin/src/style/utils.css | 4 + grafana-plugin/src/style/vars.css | 23 +- grafana-plugin/src/utils/DOM.ts | 4 + 29 files changed, 1137 insertions(+), 187 deletions(-) delete mode 100644 grafana-plugin/src/components/ScheduleQuality/ScheduleQuality.module.css create mode 100644 grafana-plugin/src/components/ScheduleQuality/ScheduleQuality.module.scss create mode 100644 grafana-plugin/src/components/ScheduleQualityDetails/ScheduleQualityDetails.module.scss create mode 100644 grafana-plugin/src/components/ScheduleQualityDetails/ScheduleQualityDetails.tsx create mode 100644 grafana-plugin/src/components/ScheduleQualityDetails/ScheduleQualityProgressBar.module.scss create mode 100644 grafana-plugin/src/components/ScheduleQualityDetails/ScheduleQualityProgressBar.test.tsx create mode 100644 grafana-plugin/src/components/ScheduleQualityDetails/ScheduleQualityProgressBar.tsx create mode 100644 grafana-plugin/src/components/ScheduleQualityDetails/__snapshots__/ScheduleQualityProgressBar.test.tsx.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e7e23bc..d0f32cc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -140,6 +140,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add direct user paging ([823](https://github.com/grafana/oncall/issues/823)) - Add App Store link to web UI ([1328](https://github.com/grafana/oncall/pull/1328)) +- Added Schedule Score quality within the schedule view ([118](https://github.com/grafana/oncall/issues/118)) ### Fixed diff --git a/grafana-plugin/src/components/ScheduleCounter/ScheduleCounter.module.css b/grafana-plugin/src/components/ScheduleCounter/ScheduleCounter.module.css index b0eea531..6dc02ce6 100644 --- a/grafana-plugin/src/components/ScheduleCounter/ScheduleCounter.module.css +++ b/grafana-plugin/src/components/ScheduleCounter/ScheduleCounter.module.css @@ -1,28 +1,29 @@ .root { font-size: 12px; line-height: 16px; + padding: 3px 4px; } .root__type_link { - padding: 2px 4px; background: rgba(27, 133, 94, 0.15); - border: 1px solid var(--success-text-color); + border: 1px solid var(--tag-border-success); border-radius: 2px; } .root__type_warning { - padding: 2px 4px; background: rgba(245, 183, 61, 0.18); - border: 1px solid var(--warning-text-color); + border: 1px solid var(--tag-border-warning); border-radius: 2px; } +.text__type_link, .icon__type_link { - color: var(--success-text-color); + color: var(--tag-text-success); } +.text__type_warning, .icon__type_warning { - color: var(--warning-text-color); + color: var(--tag-text-warning); } .tooltip { diff --git a/grafana-plugin/src/components/ScheduleCounter/ScheduleCounter.tsx b/grafana-plugin/src/components/ScheduleCounter/ScheduleCounter.tsx index 45c17f99..4762d5f8 100644 --- a/grafana-plugin/src/components/ScheduleCounter/ScheduleCounter.tsx +++ b/grafana-plugin/src/components/ScheduleCounter/ScheduleCounter.tsx @@ -12,7 +12,8 @@ interface ScheduleCounterProps { count: number; tooltipTitle: string; tooltipContent: React.ReactNode; - onHover: () => void; + addPadding?: boolean; + onHover?: () => void; } const typeToIcon = { @@ -20,15 +21,10 @@ const typeToIcon = { warning: 'exclamation-triangle', }; -const typeToColor = { - link: 'success', - warning: 'warning', -}; - const cx = cn.bind(styles); const ScheduleCounter: FC = (props) => { - const { type, count, tooltipTitle, tooltipContent, onHover } = props; + const { type, count, tooltipTitle, tooltipContent, onHover, addPadding } = props; return ( = (props) => { content={
- {tooltipTitle} + {tooltipTitle} {tooltipContent}
} > -
+
- {count} + {count}
diff --git a/grafana-plugin/src/components/ScheduleQuality/ScheduleQuality.module.css b/grafana-plugin/src/components/ScheduleQuality/ScheduleQuality.module.css deleted file mode 100644 index 24f1ab48..00000000 --- a/grafana-plugin/src/components/ScheduleQuality/ScheduleQuality.module.css +++ /dev/null @@ -1,46 +0,0 @@ -.root { - padding: 4px 10px; - gap: 10px; - background: var(--primary-background); - border: var(--border-medium); - border-radius: 2px; -} - -.details { - width: auto; - padding: 10px 0; -} - -.progress { - width: 100%; - height: 16px; - background-color: var(--secondary-background); - position: relative; -} - -.progress-filler { - height: 100%; - position: absolute; -} - -.progress-filler__type_success { - background-color: var(--success-text-color); -} - -.progress-filler__type_warning { - background-color: var(--warning-text-color); -} - -.quality-text { - float: right; - line-height: 16px; - margin-right: 3px; -} - -.quality-text__type_success { - color: var(--primary-text-color); -} - -.quality-text__type_warning { - color: #111217; -} diff --git a/grafana-plugin/src/components/ScheduleQuality/ScheduleQuality.module.scss b/grafana-plugin/src/components/ScheduleQuality/ScheduleQuality.module.scss new file mode 100644 index 00000000..1ca938e9 --- /dev/null +++ b/grafana-plugin/src/components/ScheduleQuality/ScheduleQuality.module.scss @@ -0,0 +1,39 @@ +$score-primary: rgba(27, 133, 94, 0.15); +$score-warning: rgba(245, 183, 61, 0.18); +$score-danger: rgba(209, 14, 92, 0.15); + +.root { + display: flex; + flex-direction: row; + align-items: center; + gap: 5px; +} + +.quality { + line-height: 16px; +} + +.link { + text-decoration: none !important; +} + +.tag { + font-size: 12px; + padding: 4px 10px 3px 10px; + + &--danger { + background-color: $score-danger; + color: var(--tag-text-danger); + border: 1px solid var(--tag-border-danger); + } + &--warning { + background-color: $score-warning; + color: var(--tag-text-warning); + border: 1px solid var(--tag-border-warning); + } + &--primary { + background-color: $score-primary; + color: var(--tag-text-success); + border: 1px solid var(--tag-border-success); + } +} diff --git a/grafana-plugin/src/components/ScheduleQuality/ScheduleQuality.tsx b/grafana-plugin/src/components/ScheduleQuality/ScheduleQuality.tsx index 850ae043..6deecb69 100644 --- a/grafana-plugin/src/components/ScheduleQuality/ScheduleQuality.tsx +++ b/grafana-plugin/src/components/ScheduleQuality/ScheduleQuality.tsx @@ -1,96 +1,131 @@ -import React, { FC, useCallback, useState } from 'react'; +import React, { FC, useEffect, useState } from 'react'; -import { HorizontalGroup, VerticalGroup, Icon, IconButton, Tooltip } from '@grafana/ui'; +import { Tooltip, VerticalGroup } from '@grafana/ui'; import cn from 'classnames/bind'; +import PluginLink from 'components/PluginLink/PluginLink'; +import ScheduleCounter from 'components/ScheduleCounter/ScheduleCounter'; +import { ScheduleQualityDetails } from 'components/ScheduleQualityDetails/ScheduleQualityDetails'; +import Tag from 'components/Tag/Tag'; import Text from 'components/Text/Text'; +import { Schedule, ScheduleScoreQualityResponse, ScheduleScoreQualityResult } from 'models/schedule/schedule.types'; +import { useStore } from 'state/useStore'; -import styles from './ScheduleQuality.module.css'; - -interface ScheduleQualityProps { - quality: number; -} +import styles from './ScheduleQuality.module.scss'; const cx = cn.bind(styles); -const ScheduleQuality: FC = (props) => { - const { quality } = props; - - return ( - }> -
- - Quality: - {Math.floor(quality * 100)}% - -
-
- ); -}; - -interface ScheduleQualityDetailsProps { - quality: number; +interface ScheduleQualityProps { + schedule: Schedule; + lastUpdated: number; } -const SheduleQualityDetails = (props: ScheduleQualityDetailsProps) => { - const { quality } = props; +const ScheduleQuality: FC = ({ schedule, lastUpdated }) => { + const { scheduleStore } = useStore(); + const [qualityResponse, setQualityResponse] = useState(undefined); - const [expanded, setExpanded] = useState(false); + useEffect(() => { + if (schedule.id) { + fetchScoreQuality(); + } + }, [schedule.id, lastUpdated]); - const type = quality > 0.8 ? 'success' : 'warning'; + if (!qualityResponse) { + return null; + } - const qualityPercent = quality * 100; - - const handleExpandClick = useCallback(() => { - setExpanded((expanded) => !expanded); - }, []); + const relatedEscalationChains = scheduleStore.relatedEscalationChains[schedule.id]; return ( -
- - Schedule quality -
-
-
- {qualityPercent}% -
{' '} -
-
- {type === 'success' && ( - - You are doing a great job!
- Schedule is well balanced for all members. -
+ <> +
+ {relatedEscalationChains?.length > 0 && schedule?.number_of_escalation_chains > 0 && ( + + {relatedEscalationChains.map((escalationChain) => ( +
+ + {escalationChain.name} + +
+ ))} + + } + /> )} - {type === 'warning' && Your schedule has balance problems.} -
- - - - - Calculation methodology - - - - {expanded && ( - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer elementum purus egestas porta ultricies. - Sed quis maximus sem. Phasellus semper pulvinar sapien ac euismod. - - )} - - -
+ + {schedule.warnings?.length > 0 && ( + + {schedule.warnings.map((warning, index) => ( + + {warning} + + ))} +
+ } + /> + )} + + + } + > +
+ + Quality: {getScheduleQualityString(qualityResponse.total_score)} + +
+
+
+ ); + + function getScheduleQualityString(score: number): ScheduleScoreQualityResult { + if (score < 20) { + return ScheduleScoreQualityResult.Bad; + } + if (score < 40) { + return ScheduleScoreQualityResult.Low; + } + if (score < 60) { + return ScheduleScoreQualityResult.Medium; + } + if (score < 80) { + return ScheduleScoreQualityResult.Good; + } + return ScheduleScoreQualityResult.Great; + } + + async function fetchScoreQuality() { + await Promise.all([ + scheduleStore.getScoreQuality(schedule.id).then((qualityResponse) => setQualityResponse(qualityResponse)), + scheduleStore.updateRelatedEscalationChains(schedule.id), + ]); + } + + function getTagClass() { + if (qualityResponse?.total_score < 20) { + return 'tag--danger'; + } + if (qualityResponse?.total_score < 60) { + return 'tag--warning'; + } + return 'tag--primary'; + } }; export default ScheduleQuality; diff --git a/grafana-plugin/src/components/ScheduleQualityDetails/ScheduleQualityDetails.module.scss b/grafana-plugin/src/components/ScheduleQualityDetails/ScheduleQualityDetails.module.scss new file mode 100644 index 00000000..97e6b8f6 --- /dev/null +++ b/grafana-plugin/src/components/ScheduleQualityDetails/ScheduleQualityDetails.module.scss @@ -0,0 +1,61 @@ +$padding: 8px; +$width: 280px; + +.root { + width: $width; + margin-left: -8px; + margin-right: -8px; +} + +.container { + display: flex; + flex-wrap: wrap; + flex-direction: column; + width: 100%; + + &--withTopPadding { + padding-top: $padding; + } + + &--withLateralPadding { + padding-left: $padding; + padding-right: $padding; + } +} + +.header { + padding-bottom: calc($padding / 2); +} +.header__subText { + font-weight: 500; +} + +.row { + display: flex; + flex-direction: row; + column-gap: 8px; + margin-bottom: 4px; +} + +.line-break { + width: 100%; + border-top: 1px solid var(--always-gray); + margin-top: 8px; + opacity: 15%; +} + +.metholodogy { + padding: 4px 0px; +} + +.text { + word-wrap: break-word; + padding-left: 24px; +} + +.email { + max-width: calc($width - $padding); + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} diff --git a/grafana-plugin/src/components/ScheduleQualityDetails/ScheduleQualityDetails.tsx b/grafana-plugin/src/components/ScheduleQualityDetails/ScheduleQualityDetails.tsx new file mode 100644 index 00000000..a30aa735 --- /dev/null +++ b/grafana-plugin/src/components/ScheduleQualityDetails/ScheduleQualityDetails.tsx @@ -0,0 +1,164 @@ +import React, { FC, useCallback, useEffect, useState } from 'react'; + +import { HorizontalGroup, Icon, IconButton } from '@grafana/ui'; +import cn from 'classnames/bind'; +import dayjs from 'dayjs'; + +import Text from 'components/Text/Text'; +import { ScheduleScoreQualityResponse, ScheduleScoreQualityResult } from 'models/schedule/schedule.types'; +import { getTzOffsetString } from 'models/timezone/timezone.helpers'; +import { User } from 'models/user/user.types'; +import { useStore } from 'state/useStore'; +import { getVar } from 'utils/DOM'; + +import styles from './ScheduleQualityDetails.module.scss'; +import { ScheduleQualityProgressBar } from './ScheduleQualityProgressBar'; + +const cx = cn.bind(styles); + +interface ScheduleQualityDetailsProps { + quality: ScheduleScoreQualityResponse; + getScheduleQualityString: (score: number) => ScheduleScoreQualityResult; +} + +export const ScheduleQualityDetails: FC = ({ quality, getScheduleQualityString }) => { + const { userStore } = useStore(); + const { total_score: score, comments, overloaded_users } = quality; + const [expanded, setExpanded] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [overloadedUsers, setOverloadedUsers] = useState([]); + + useEffect(() => { + fetchUsers(); + }, []); + + const handleExpandClick = useCallback(() => { + setExpanded((expanded) => !expanded); + }, []); + + if (isLoading) { + return null; + } + + const infoComments = comments.filter((c) => c.type === 'info'); + const warningComments = comments.filter((c) => c.type === 'warning'); + + return ( +
+
+
+ + Schedule quality:{' '} + + {getScheduleQualityString(score)} + + + +
+ +
+ {comments?.length > 0 && ( + <> + {/* Show Info comments */} + {infoComments?.length > 0 && ( +
+
+ +
+ {infoComments.map((comment, index) => ( + + {comment.text} + + ))} +
+
+
+ )} + + {/* Show Warning comments afterwards */} + {warningComments?.length > 0 && ( +
+
+ +
+ Rotation structure issues + {warningComments.map((comment, index) => ( + + {comment.text} + + ))} +
+
+
+ )} + + )} + + {overloadedUsers?.length > 0 && ( +
+
+ +
+ Overloaded users + {overloadedUsers.map((overloadedUser, index) => ( + + {overloadedUser.email} ({getTzOffsetString(dayjs().tz(overloadedUser.timezone))}) + + ))} +
+
+
+ )} +
+ +
+ +
+ + + + + Calculation methodology + + + + + {expanded && ( + + The latest 90 days are taken into consideration when calculating the overall schedule quality. + + )} +
+
+
+ ); + + async function fetchUsers() { + if (!overloaded_users?.length) { + setIsLoading(false); + return; + } + + const allUsersList: User[] = userStore.getSearchResult().results; + const overloadedUsers = []; + + allUsersList.forEach((user) => { + if (overloaded_users.indexOf(user['pk']) !== -1) { + overloadedUsers.push(user); + } + }); + + setIsLoading(false); + setOverloadedUsers(overloadedUsers); + } + + function getScheduleQualityMatchingColor(score: number): string { + if (score < 20) { + return getVar('--tag-text-danger'); + } + if (score < 60) { + return getVar('--tag-text-warning'); + } + return getVar('--tag-text-success'); + } +}; diff --git a/grafana-plugin/src/components/ScheduleQualityDetails/ScheduleQualityProgressBar.module.scss b/grafana-plugin/src/components/ScheduleQualityDetails/ScheduleQualityProgressBar.module.scss new file mode 100644 index 00000000..bef62e0a --- /dev/null +++ b/grafana-plugin/src/components/ScheduleQualityDetails/ScheduleQualityProgressBar.module.scss @@ -0,0 +1,43 @@ +$border-radius: 2px; + +@mixin progressBar($color) { + background-color: $color; + border-radius: $border-radius; + height: 8px; +} + +.c-progressBar__wrapper { + width: 100%; + height: 8px; + display: flex; + gap: 2px; +} + +.c-progressBar__bar { + height: 8px; +} + +.c-progressBar__bar--warning { + background-color: var(--warning-text-color); +} +.c-progressBar__bar--error { + background-color: var(--error-text-color); +} +.c-progressBar__bar--primary { + background-color: var(--success-text-color); +} + +.c-progressBar__row { + background-color: var(--gray-8); +} + +.c-progressBar__row:first-child, +.c-progressBar__row:first-child > .c-progressBar__bar { + border-top-left-radius: $border-radius; + border-bottom-left-radius: $border-radius; +} +.c-progressBar__row:last-child, +.c-progressBar__row:last-child > .c-progressBar__bar { + border-top-right-radius: $border-radius; + border-bottom-right-radius: $border-radius; +} diff --git a/grafana-plugin/src/components/ScheduleQualityDetails/ScheduleQualityProgressBar.test.tsx b/grafana-plugin/src/components/ScheduleQualityDetails/ScheduleQualityProgressBar.test.tsx new file mode 100644 index 00000000..0f348afa --- /dev/null +++ b/grafana-plugin/src/components/ScheduleQualityDetails/ScheduleQualityProgressBar.test.tsx @@ -0,0 +1,58 @@ +import 'jest/matchMedia.ts'; +import React from 'react'; + +import { render, screen } from '@testing-library/react'; + +import { ScheduleQualityProgressBar } from './ScheduleQualityProgressBar'; + +const NUM_STEPS = 5; +const DANGER_CLASS = 'c-progressBar__bar--danger'; +const WARNING_CLASS = 'c-progressBar__bar--warning'; +const SUCCESS_CLASS = 'c-progressBar__bar--primary'; + +describe('SourceCode', () => { + test('It renders 0% complete', () => { + render(); + + expect(screen.queryAllByTestId('progressBar__bar').length).toEqual(NUM_STEPS); + const allBars = screen.queryAllByTestId('progressBar__bar'); + allBars.forEach((bar) => expect(bar.getAttribute('style').includes('width: 0%'))); + }); + + test('It renders 100% complete', () => { + render(); + + expect(screen.queryAllByTestId('progressBar__bar').length).toEqual(NUM_STEPS); + const allBars = screen.queryAllByTestId('progressBar__bar'); + allBars.forEach((bar) => expect(bar.getAttribute('style').includes('width: 100%'))); + }); + + test.each([0, 25, 30, 50, 65, 70, 100])('It renders at %p%', (completed) => { + const component = render(); + expect(component.container).toMatchSnapshot(); + }); + + test.each([0, 10, 19])('It renders as danger at <20% completion', (completed) => { + render(); + + screen + .queryAllByTestId('progress__bar') + .forEach((elem) => expect(Array.from(elem.classList).includes(DANGER_CLASS))); + }); + + test.each([20, 31, 41, 61])('It renders as warning at <60% completion', (completed) => { + render(); + + screen + .queryAllByTestId('progress__bar') + .forEach((elem) => expect(Array.from(elem.classList).includes(WARNING_CLASS))); + }); + + test.each([60, 61, 79, 99, 100])('It renders as success at >=60% completion', (completed) => { + render(); + + screen + .queryAllByTestId('progress__bar') + .forEach((elem) => expect(Array.from(elem.classList).includes(SUCCESS_CLASS))); + }); +}); diff --git a/grafana-plugin/src/components/ScheduleQualityDetails/ScheduleQualityProgressBar.tsx b/grafana-plugin/src/components/ScheduleQualityDetails/ScheduleQualityProgressBar.tsx new file mode 100644 index 00000000..c03f4052 --- /dev/null +++ b/grafana-plugin/src/components/ScheduleQualityDetails/ScheduleQualityProgressBar.tsx @@ -0,0 +1,87 @@ +import React from 'react'; + +import cn from 'classnames/bind'; + +import styles from './ScheduleQualityProgressBar.module.scss'; + +interface ProgressBarProps { + completed: number; + className?: string; + numTotalSteps?: number; +} + +const cx = cn.bind(styles); + +export const ScheduleQualityProgressBar: React.FC = ({ className, completed, numTotalSteps }) => { + const classList = ['c-progressBar__bar', className || '']; + + return ( +
+ {!numTotalSteps &&
} + {renderSteps(numTotalSteps, completed)} +
+ ); + + function renderSteps(numTotalSteps: number, completed: number) { + if (!numTotalSteps) { + return null; + } + + const maxFillPerRow = 100 / numTotalSteps; + const rowFill = calculateRowFill(numTotalSteps, completed); + + return new Array(numTotalSteps).fill(0).map((_row, index) => { + const percentWidth = rowFill[index]; + + return ( +
+
+
+ ); + }); + } + + function getClassForCompletionLevel() { + if (completed < 20) { + return 'c-progressBar__bar--danger'; + } + if (completed < 60) { + return 'c-progressBar__bar--warning'; + } + return 'c-progressBar__bar--primary'; + } + + function calculateRowFill(numTotalSteps: number, completed: number): number[] { + const fillPerRows = []; + const maxFillPerRow = 100 / numTotalSteps; + let leftToFill = completed; + + new Array(numTotalSteps).fill(0).forEach((_value, index) => { + let currentFill: number; + + currentFill = leftToFill - maxFillPerRow < 0 ? leftToFill : maxFillPerRow; + + leftToFill -= maxFillPerRow; + + let percentWidth = Math.max(0, (currentFill * 100) / maxFillPerRow); + const shouldSetMinValueInitially = completed > 0 && Math.floor(percentWidth) === 0 && !index; + + if (shouldSetMinValueInitially) { + percentWidth = 1; + } + + fillPerRows.push(percentWidth); + }); + + return fillPerRows; + } +}; diff --git a/grafana-plugin/src/components/ScheduleQualityDetails/__snapshots__/ScheduleQualityProgressBar.test.tsx.snap b/grafana-plugin/src/components/ScheduleQualityDetails/__snapshots__/ScheduleQualityProgressBar.test.tsx.snap new file mode 100644 index 00000000..d02f6728 --- /dev/null +++ b/grafana-plugin/src/components/ScheduleQualityDetails/__snapshots__/ScheduleQualityProgressBar.test.tsx.snap @@ -0,0 +1,449 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SourceCode It renders at 0% 1`] = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`SourceCode It renders at 25% 1`] = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`SourceCode It renders at 30% 1`] = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`SourceCode It renders at 50% 1`] = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`SourceCode It renders at 65% 1`] = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`SourceCode It renders at 70% 1`] = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`SourceCode It renders at 100% 1`] = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/grafana-plugin/src/components/Tag/Tag.module.css b/grafana-plugin/src/components/Tag/Tag.module.css index a203c311..6fc0c7e5 100644 --- a/grafana-plugin/src/components/Tag/Tag.module.css +++ b/grafana-plugin/src/components/Tag/Tag.module.css @@ -1,5 +1,5 @@ .root { - border-radius: 4px; + border-radius: 2px; padding: 1px 7px 4px 7px; color: white; } diff --git a/grafana-plugin/src/components/Tag/Tag.tsx b/grafana-plugin/src/components/Tag/Tag.tsx index 61037524..088842cb 100644 --- a/grafana-plugin/src/components/Tag/Tag.tsx +++ b/grafana-plugin/src/components/Tag/Tag.tsx @@ -5,7 +5,7 @@ import cn from 'classnames/bind'; import styles from 'components/Tag/Tag.module.css'; interface TagProps { - color: string; + color?: string; className?: string; children?: any; onClick?: (ev) => void; @@ -16,14 +16,14 @@ const cx = cn.bind(styles); const Tag: FC = (props) => { const { children, color, className, onClick } = props; + const style: React.CSSProperties = {}; + + if (color) { + style.backgroundColor = color; + } return ( - + {children} ); diff --git a/grafana-plugin/src/components/Text/Text.module.scss b/grafana-plugin/src/components/Text/Text.module.scss index 618bb298..dfcba488 100644 --- a/grafana-plugin/src/components/Text/Text.module.scss +++ b/grafana-plugin/src/components/Text/Text.module.scss @@ -23,6 +23,10 @@ color: var(--primary-text-link); } + &--danger { + color: var(--error-text-color); + } + &--success { color: var(--green-5); } diff --git a/grafana-plugin/src/components/Text/Text.tsx b/grafana-plugin/src/components/Text/Text.tsx index 1570635f..a6c87f0f 100644 --- a/grafana-plugin/src/components/Text/Text.tsx +++ b/grafana-plugin/src/components/Text/Text.tsx @@ -8,7 +8,7 @@ import { openNotification } from 'utils'; import styles from './Text.module.scss'; -export type TextType = 'primary' | 'secondary' | 'disabled' | 'link' | 'success' | 'warning'; +export type TextType = 'primary' | 'secondary' | 'disabled' | 'link' | 'success' | 'warning' | 'danger'; interface TextProps extends HTMLAttributes { type?: TextType; diff --git a/grafana-plugin/src/containers/AlertRules/parts/index.tsx b/grafana-plugin/src/containers/AlertRules/parts/index.tsx index bcf2f0b4..76d51bcc 100644 --- a/grafana-plugin/src/containers/AlertRules/parts/index.tsx +++ b/grafana-plugin/src/containers/AlertRules/parts/index.tsx @@ -7,6 +7,7 @@ import SlackConnector from 'containers/AlertRules/parts/connectors/SlackConnecto import TelegramConnector from 'containers/AlertRules/parts/connectors/TelegramConnector'; import { ChannelFilter } from 'models/channel_filter'; import { useStore } from 'state/useStore'; +import { getVar } from 'utils/DOM'; interface ChatOpsConnectorsProps { channelFilterId: ChannelFilter['id']; @@ -26,7 +27,7 @@ export const ChatOpsConnectors = (props: ChatOpsConnectorsProps) => { } return ( - + {isSlackInstalled && } {isTelegramInstalled && } diff --git a/grafana-plugin/src/containers/EscalationChainSteps/EscalationChainSteps.tsx b/grafana-plugin/src/containers/EscalationChainSteps/EscalationChainSteps.tsx index 1ddb7306..80af9ad7 100644 --- a/grafana-plugin/src/containers/EscalationChainSteps/EscalationChainSteps.tsx +++ b/grafana-plugin/src/containers/EscalationChainSteps/EscalationChainSteps.tsx @@ -12,6 +12,7 @@ import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/W import { EscalationChain } from 'models/escalation_chain/escalation_chain.types'; import { EscalationPolicyOption } from 'models/escalation_policy/escalation_policy.types'; import { useStore } from 'state/useStore'; +import { getVar } from 'utils/DOM'; import { UserActions } from 'utils/authorization'; import styles from './EscalationChainSteps.module.css'; @@ -92,10 +93,7 @@ const EscalationChainSteps = observer((props: EscalationChainStepsProps) => { ) : ( )} - +