From c45da45300ee117f2caae444bb5c7dc53308d008 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 7 Sep 2022 13:48:55 +0300 Subject: [PATCH 01/28] changes for add token form --- ...Code.module.css => SourceCode.module.scss} | 20 +++--- .../src/components/SourceCode/SourceCode.tsx | 17 +++-- .../ApiTokenSettings/ApiTokenForm.module.css | 12 ++++ .../ApiTokenSettings/ApiTokenForm.tsx | 70 ++++++++++++++----- 4 files changed, 87 insertions(+), 32 deletions(-) rename grafana-plugin/src/components/SourceCode/{SourceCode.module.css => SourceCode.module.scss} (63%) create mode 100644 grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.module.css diff --git a/grafana-plugin/src/components/SourceCode/SourceCode.module.css b/grafana-plugin/src/components/SourceCode/SourceCode.module.scss similarity index 63% rename from grafana-plugin/src/components/SourceCode/SourceCode.module.css rename to grafana-plugin/src/components/SourceCode/SourceCode.module.scss index beabde1eef..baff26d541 100644 --- a/grafana-plugin/src/components/SourceCode/SourceCode.module.css +++ b/grafana-plugin/src/components/SourceCode/SourceCode.module.scss @@ -1,29 +1,33 @@ .root { position: relative; width: 100%; + + &:hover .button { + opacity: 1; + } } .scroller { overflow-y: auto; -} -.scroller_max-height { - max-height: 400px; + &--maxHeight { + max-height: 400px; + } } -.root .button { +.button { position: absolute; top: 15px; right: 15px; opacity: 0; transition: opacity 0.2s ease; -} -.root:hover .button { - opacity: 1; + &--top { + top: 4px; + } } -.root pre { +pre { border-radius: 2px; padding: 12px 20px; } diff --git a/grafana-plugin/src/components/SourceCode/SourceCode.tsx b/grafana-plugin/src/components/SourceCode/SourceCode.tsx index 91c3513cbf..61fc220266 100644 --- a/grafana-plugin/src/components/SourceCode/SourceCode.tsx +++ b/grafana-plugin/src/components/SourceCode/SourceCode.tsx @@ -6,18 +6,19 @@ import CopyToClipboard from 'react-copy-to-clipboard'; import { openNotification } from 'utils'; -import styles from './SourceCode.module.css'; +import styles from './SourceCode.module.scss'; const cx = cn.bind(styles); interface SourceCodeProps { noMaxHeight?: boolean; showCopyToClipboard?: boolean; - children?: any + isButtonTopPositioned?: boolean; + children?: any; } const SourceCode: FC = (props) => { - const { children, noMaxHeight = false, showCopyToClipboard = true } = props; + const { children, isButtonTopPositioned = false, noMaxHeight = false, showCopyToClipboard = true } = props; return (
@@ -28,14 +29,20 @@ const SourceCode: FC = (props) => { openNotification('Copied!'); }} > - )}
         {children}
diff --git a/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.module.css b/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.module.css
new file mode 100644
index 0000000000..bae192e639
--- /dev/null
+++ b/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.module.css
@@ -0,0 +1,12 @@
+.token__inputContainer {
+  width: 100%;
+  display: flex;
+  margin-bottom: 24px;
+}
+
+.token__input {
+  flex-grow: 1;
+}
+.token__copyButton {
+  margin-left: 12px;
+}
diff --git a/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.tsx b/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.tsx
index 8de6d26909..8585502184 100644
--- a/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.tsx
+++ b/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.tsx
@@ -1,6 +1,7 @@
 import React, { useCallback, HTMLAttributes, useState } from 'react';
 
-import { Button, Field, HorizontalGroup, Input, Modal, VerticalGroup } from '@grafana/ui';
+import { Button, HorizontalGroup, Input, Label, Modal, VerticalGroup } from '@grafana/ui';
+import cn from 'classnames/bind';
 import { get } from 'lodash-es';
 import { observer } from 'mobx-react';
 import CopyToClipboard from 'react-copy-to-clipboard';
@@ -9,6 +10,13 @@ import { ApiToken } from 'models/api_token/api_token.types';
 import { useStore } from 'state/useStore';
 import { openErrorNotification, openNotification } from 'utils';
 
+import styles from './ApiTokenForm.module.css';
+import SourceCode from 'components/SourceCode/SourceCode';
+
+const cx = cn.bind(styles);
+
+const CURL_EXAMPLE = `curl: try 'curl --help' or 'curl --manual' for more information`;
+
 interface TokenCreationModalProps extends HTMLAttributes {
   visible: boolean;
   onHide: () => void;
@@ -16,9 +24,10 @@ interface TokenCreationModalProps extends HTMLAttributes {
 }
 
 const ApiTokenForm = observer((props: TokenCreationModalProps) => {
-  const { visible, onHide = () => {}, onUpdate = () => {} } = props;
+  const { onHide = () => {}, onUpdate = () => {} } = props;
   const [name, setName] = useState('');
   const [token, setToken] = useState('');
+  const [isModalOpen, setIsModalOpen] = useState(true);
 
   const store = useStore();
 
@@ -37,32 +46,55 @@ const ApiTokenForm = observer((props: TokenCreationModalProps) => {
   }, []);
 
   return (
-    
+    
       
-        
-        {token && (
-          <>
-            
-          
-        )}
-        
+        
+        
+ {renderTokenInput()} + {token && ( - { - openNotification('Token copied'); - }} - > - + openNotification('Token copied')}> + )} -
+ + + {CURL_EXAMPLE} + + + + {!token && ( + + )}
); + + function renderTokenInput() { + if (!token) + return ( + + ); + + return ; + } }); export default ApiTokenForm; From b00d8dab3a268d5d8a3dc4c144a216f7f330473f Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 7 Sep 2022 13:54:02 +0300 Subject: [PATCH 02/28] alignment change --- grafana-plugin/src/components/SourceCode/SourceCode.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grafana-plugin/src/components/SourceCode/SourceCode.module.scss b/grafana-plugin/src/components/SourceCode/SourceCode.module.scss index baff26d541..fc7c3b9f87 100644 --- a/grafana-plugin/src/components/SourceCode/SourceCode.module.scss +++ b/grafana-plugin/src/components/SourceCode/SourceCode.module.scss @@ -23,7 +23,7 @@ transition: opacity 0.2s ease; &--top { - top: 4px; + top: 6px; } } From 55119a44538d0896ccf1561e89eaa56482bb58a1 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 7 Sep 2022 14:28:12 +0300 Subject: [PATCH 03/28] fixed modal not opening after creating a new token --- .../ApiTokenSettings/ApiTokenForm.module.css | 1 + .../src/containers/ApiTokenSettings/ApiTokenForm.tsx | 11 +++++------ .../containers/ApiTokenSettings/ApiTokenSettings.tsx | 2 -- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.module.css b/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.module.css index bae192e639..09f033e012 100644 --- a/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.module.css +++ b/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.module.css @@ -7,6 +7,7 @@ .token__input { flex-grow: 1; } + .token__copyButton { margin-left: 12px; } diff --git a/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.tsx b/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.tsx index 8585502184..d37a0b5729 100644 --- a/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.tsx +++ b/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.tsx @@ -6,12 +6,12 @@ import { get } from 'lodash-es'; import { observer } from 'mobx-react'; import CopyToClipboard from 'react-copy-to-clipboard'; +import SourceCode from 'components/SourceCode/SourceCode'; import { ApiToken } from 'models/api_token/api_token.types'; import { useStore } from 'state/useStore'; import { openErrorNotification, openNotification } from 'utils'; import styles from './ApiTokenForm.module.css'; -import SourceCode from 'components/SourceCode/SourceCode'; const cx = cn.bind(styles); @@ -27,7 +27,6 @@ const ApiTokenForm = observer((props: TokenCreationModalProps) => { const { onHide = () => {}, onUpdate = () => {} } = props; const [name, setName] = useState(''); const [token, setToken] = useState(''); - const [isModalOpen, setIsModalOpen] = useState(true); const store = useStore(); @@ -47,7 +46,7 @@ const ApiTokenForm = observer((props: TokenCreationModalProps) => { return ( { {CURL_EXAMPLE} - {!token && ( @@ -83,7 +82,7 @@ const ApiTokenForm = observer((props: TokenCreationModalProps) => { function renderTokenInput() { if (!token) - return ( + {return ( { placeholder="Enter token name" autoFocus /> - ); + );} return ; } diff --git a/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenSettings.tsx b/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenSettings.tsx index 71e85426b4..f9a9f0bbdf 100644 --- a/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenSettings.tsx +++ b/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenSettings.tsx @@ -48,8 +48,6 @@ class ApiTokens extends React.Component { const apiTokens = apiTokenStore.getSearchResult(); - const loading = !apiTokens; - const { showCreateTokenModal } = this.state; const columns = [ From 804da53982eeec293dc42981778ca8424470dd7a Mon Sep 17 00:00:00 2001 From: Yulia Shanyrova Date: Thu, 8 Sep 2022 13:48:36 +0200 Subject: [PATCH 04/28] smal bugs fixes: link color and column naming --- .../containers/AlertRules/AlertRules.module.css | 4 ++++ .../src/containers/AlertRules/AlertRules.tsx | 14 ++++++++------ .../DefaultPageLayout/DefaultPageLayout.module.css | 4 ++++ .../DefaultPageLayout/DefaultPageLayout.tsx | 7 ++++++- grafana-plugin/src/pages/incidents/Incidents.tsx | 2 +- 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/grafana-plugin/src/containers/AlertRules/AlertRules.module.css b/grafana-plugin/src/containers/AlertRules/AlertRules.module.css index 7d76aae7be..e85a4acb3e 100644 --- a/grafana-plugin/src/containers/AlertRules/AlertRules.module.css +++ b/grafana-plugin/src/containers/AlertRules/AlertRules.module.css @@ -112,3 +112,7 @@ .slack-channel-switch { margin-left: -8px; } + +.description-style a { + color: var(--primary-text-link); +} diff --git a/grafana-plugin/src/containers/AlertRules/AlertRules.tsx b/grafana-plugin/src/containers/AlertRules/AlertRules.tsx index 68e9f1e1d4..5c4d599625 100644 --- a/grafana-plugin/src/containers/AlertRules/AlertRules.tsx +++ b/grafana-plugin/src/containers/AlertRules/AlertRules.tsx @@ -280,12 +280,14 @@ class AlertRules extends React.Component {
{alertReceiveChannel.description && ( - } - severity="info" - /> +
+
} + severity="info" + /> + )}
diff --git a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.module.css b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.module.css index d80b7131cc..e18d054528 100644 --- a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.module.css +++ b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.module.css @@ -18,3 +18,7 @@ line-height: 20px; height: auto; } + +.instructions-link { + color: var(--primary-text-link); +} diff --git a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx index fd726b4ab2..06093eb41b 100644 --- a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx +++ b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx @@ -109,7 +109,12 @@ const DefaultPageLayout: FC = observer((props) => { {`Current plugin version: ${plugin.version}, current engine version: ${store.backendVersion}`}
Please see{' '} - + the update instructions . diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index e28398581d..63fb1c4b44 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -321,7 +321,7 @@ class Incidents extends React.Component }, { width: '15%', - title: 'Source', + title: 'Integrations', key: 'source', render: withSkeleton(this.renderSource), }, From fa0d854ee52d3accef7b70cf87fd6e869ad93029 Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Thu, 8 Sep 2022 11:56:04 -0600 Subject: [PATCH 05/28] Fix log message not displaying primary key for organization being deleted --- engine/apps/user_management/sync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/apps/user_management/sync.py b/engine/apps/user_management/sync.py index b81331869b..7b0c91d7b7 100644 --- a/engine/apps/user_management/sync.py +++ b/engine/apps/user_management/sync.py @@ -97,7 +97,7 @@ def cleanup_organization(organization_pk): if delete_organization_if_needed(organization): logger.info( f"Deleting organization due to stack deletion. " - f"pk: {organization.pk}, stack_id: {organization.stack_id}, org_id: {organization.org_id}" + f"pk: {organization_pk}, stack_id: {organization.stack_id}, org_id: {organization.org_id}" ) else: logger.info(f"Organization {organization_pk} not deleted in gcom, no action taken") From 65420d77999bd69bb0a216c773ffb974a9681c77 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 12 Sep 2022 14:36:40 +0300 Subject: [PATCH 06/28] added curl example + minor refactoring --- .../ApiTokenSettings/ApiTokenForm.tsx | 65 +++++++++++-------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.tsx b/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.tsx index d37a0b5729..8bd60647ff 100644 --- a/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.tsx +++ b/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.tsx @@ -15,8 +15,6 @@ import styles from './ApiTokenForm.module.css'; const cx = cn.bind(styles); -const CURL_EXAMPLE = `curl: try 'curl --help' or 'curl --manual' for more information`; - interface TokenCreationModalProps extends HTMLAttributes { visible: boolean; onHide: () => void; @@ -45,26 +43,15 @@ const ApiTokenForm = observer((props: TokenCreationModalProps) => { }, []); return ( - +
{renderTokenInput()} - - {token && ( - openNotification('Token copied')}> - - - )} + {renderCopyToClipboard()}
- - {CURL_EXAMPLE} + {renderCurlExample()} + + ); + } + + function renderCurlExample() { + if (!token) return null; + return ( + + + {getCurlExample(token)} + + ); } }); +function getCurlExample(token) { + return `curl -H "Authorization: ${token}" ${document.location.origin}/api/v1/escalation_chains`; +} + export default ApiTokenForm; From 4cbb65a5cb65569a5b60722b570a55eea3bf30a9 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 12 Sep 2022 16:57:37 +0300 Subject: [PATCH 07/28] pick hostname from onCallApiUrl --- .../components/SourceCode/SourceCode.module.scss | 4 ---- .../src/components/SourceCode/SourceCode.tsx | 8 +++----- .../containers/ApiTokenSettings/ApiTokenForm.tsx | 13 +++++++++---- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/grafana-plugin/src/components/SourceCode/SourceCode.module.scss b/grafana-plugin/src/components/SourceCode/SourceCode.module.scss index fc7c3b9f87..a926c09d27 100644 --- a/grafana-plugin/src/components/SourceCode/SourceCode.module.scss +++ b/grafana-plugin/src/components/SourceCode/SourceCode.module.scss @@ -21,10 +21,6 @@ right: 15px; opacity: 0; transition: opacity 0.2s ease; - - &--top { - top: 6px; - } } pre { diff --git a/grafana-plugin/src/components/SourceCode/SourceCode.tsx b/grafana-plugin/src/components/SourceCode/SourceCode.tsx index 61fc220266..da4f8838d0 100644 --- a/grafana-plugin/src/components/SourceCode/SourceCode.tsx +++ b/grafana-plugin/src/components/SourceCode/SourceCode.tsx @@ -13,12 +13,11 @@ const cx = cn.bind(styles); interface SourceCodeProps { noMaxHeight?: boolean; showCopyToClipboard?: boolean; - isButtonTopPositioned?: boolean; children?: any; } const SourceCode: FC = (props) => { - const { children, isButtonTopPositioned = false, noMaxHeight = false, showCopyToClipboard = true } = props; + const { children, noMaxHeight = false, showCopyToClipboard = true } = props; return (
@@ -30,10 +29,9 @@ const SourceCode: FC = (props) => { }} > @@ -91,18 +94,20 @@ const ApiTokenForm = observer((props: TokenCreationModalProps) => { } function renderCurlExample() { - if (!token) return null; + if (!token) { + return null; + } return ( - {getCurlExample(token)} + {getCurlExample(token)} ); } }); function getCurlExample(token) { - return `curl -H "Authorization: ${token}" ${document.location.origin}/api/v1/escalation_chains`; + return `curl -H "Authorization: ${token}" ${getItem('onCallApiUrl')}/api/internal/v1/alert_receive_channels`; } export default ApiTokenForm; From 4ec1727ecf869eb515c22933bfc655939d3926c9 Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Mon, 12 Sep 2022 17:25:46 -0300 Subject: [PATCH 08/28] Fix support for all_day events in schedule filter_events --- engine/apps/schedules/ical_utils.py | 25 +++++++++---------- .../apps/schedules/models/on_call_schedule.py | 15 ++++++++--- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/engine/apps/schedules/ical_utils.py b/engine/apps/schedules/ical_utils.py index 12fb5bd1bd..09a0b3072f 100644 --- a/engine/apps/schedules/ical_utils.py +++ b/engine/apps/schedules/ical_utils.py @@ -175,19 +175,18 @@ def get_shifts_dict(calendar, calendar_type, schedule, datetime_start, datetime_ if type(event[ICAL_DATETIME_START].dt) == datetime.date: start = event[ICAL_DATETIME_START].dt end = event[ICAL_DATETIME_END].dt - if start <= date < end: - result_date.append( - { - "start": start, - "end": end, - "users": users, - "missing_users": missing_users, - "priority": priority, - "source": source, - "calendar_type": calendar_type, - "shift_pk": pk, - } - ) + result_date.append( + { + "start": start, + "end": end, + "users": users, + "missing_users": missing_users, + "priority": priority, + "source": source, + "calendar_type": calendar_type, + "shift_pk": pk, + } + ) else: start, end = ical_events.get_start_and_end_with_respect_to_event_type(event) result_datetime.append( diff --git a/engine/apps/schedules/models/on_call_schedule.py b/engine/apps/schedules/models/on_call_schedule.py index 5af43797aa..b3d7291143 100644 --- a/engine/apps/schedules/models/on_call_schedule.py +++ b/engine/apps/schedules/models/on_call_schedule.py @@ -3,6 +3,7 @@ import itertools import icalendar +import pytz from django.apps import apps from django.conf import settings from django.core.validators import MinLengthValidator @@ -254,12 +255,20 @@ def _resolve_schedule(self, events): if not events: return [] + def event_start_cmp_key(e): + # all day events: compare using a datetime object at 00:00 + start = e["start"] + if not isinstance(start, datetime.datetime): + start = datetime.datetime.combine(start, datetime.datetime.min.time(), tzinfo=pytz.UTC) + return start + def event_cmp_key(e): """Sorting key criteria for events.""" + start = event_start_cmp_key(e) return ( -e["calendar_type"] if e["calendar_type"] else 0, # overrides: 1, shifts: 0, gaps: None -e["priority_level"] if e["priority_level"] else 0, - e["start"], + start, ) def insort_event(eventlist, e): @@ -314,7 +323,7 @@ def _merge_intervals(evs): if ev["priority_level"] != current_priority: # update scheduled intervals on priority change # and start from the beginning for the new priority level - resolved.sort(key=lambda e: e["start"]) + resolved.sort(key=event_start_cmp_key) intervals = _merge_intervals(resolved) current_interval_idx = 0 current_priority = ev["priority_level"] @@ -367,7 +376,7 @@ def _merge_intervals(evs): # TODO: switch to bisect insert on python 3.10 (or consider heapq) insort_event(pending, ev) - resolved.sort(key=lambda e: (e["start"], e["shift"]["pk"])) + resolved.sort(key=lambda e: (event_start_cmp_key(e), e["shift"]["pk"] or "")) return resolved def _merge_events(self, events): From 9a6a7d0d1c72f29ed484ee8eb21c57017f55a7d7 Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 13 Sep 2022 16:04:54 +0300 Subject: [PATCH 09/28] fix typescript errors --- grafana-plugin/src/components/Modal/Modal.tsx | 2 +- .../ScheduleCounter/ScheduleCounter.tsx | 10 +- .../ScheduleQuality/ScheduleQuality.tsx | 1 - .../SchedulesFilters.types.ts | 10 +- grafana-plugin/src/components/Table/Table.tsx | 2 +- grafana-plugin/src/components/Text/Text.tsx | 8 +- .../TimelineMarks/TimelineMarks.tsx | 2 +- .../UserGroups/UserGroups.helpers.ts | 13 +- .../src/components/UserGroups/UserGroups.tsx | 68 +- .../components/UserGroups/UserGroups.types.ts | 1 - .../src/containers/Rotation/Rotation.tsx | 3 - .../containers/RotationForm/RotationForm.tsx | 11 +- .../RotationForm/ScheduleOverrideForm.tsx | 8 - .../src/containers/Rotations/Rotations.tsx | 3 +- .../containers/Rotations/ScheduleFinal.tsx | 7 +- .../Rotations/ScheduleOverrides.tsx | 8 +- .../containers/ScheduleForm/ScheduleForm.tsx | 12 +- .../src/models/schedule/schedule.ts | 4 +- .../src/models/schedule/schedule.types.ts | 4 +- .../src/pages/schedule/Schedule.helpers.ts | 662 +----------------- .../src/pages/schedule/Schedule.tsx | 8 +- .../pages/schedules_NEW/Schedules.helpers.ts | 42 -- .../src/pages/schedules_NEW/Schedules.tsx | 9 +- grafana-plugin/tsconfig.json | 4 +- 24 files changed, 79 insertions(+), 823 deletions(-) delete mode 100644 grafana-plugin/src/pages/schedules_NEW/Schedules.helpers.ts diff --git a/grafana-plugin/src/components/Modal/Modal.tsx b/grafana-plugin/src/components/Modal/Modal.tsx index 4cff1595ce..146a9df80a 100644 --- a/grafana-plugin/src/components/Modal/Modal.tsx +++ b/grafana-plugin/src/components/Modal/Modal.tsx @@ -8,7 +8,7 @@ ReactModal.setAppElement('#reactRoot'); import styles from './Modal.module.css'; export interface ModalProps { - title: string | JSX.Element; + title?: string | JSX.Element; className?: string; contentClassName?: string; closeOnEscape?: boolean; diff --git a/grafana-plugin/src/components/ScheduleCounter/ScheduleCounter.tsx b/grafana-plugin/src/components/ScheduleCounter/ScheduleCounter.tsx index d88013c604..9f7293c154 100644 --- a/grafana-plugin/src/components/ScheduleCounter/ScheduleCounter.tsx +++ b/grafana-plugin/src/components/ScheduleCounter/ScheduleCounter.tsx @@ -1,14 +1,14 @@ import React, { FC, useCallback } from 'react'; -import { HorizontalGroup, VerticalGroup, Icon, IconButton, Tooltip } from '@grafana/ui'; +import { HorizontalGroup, VerticalGroup, Icon, IconButton, Tooltip, IconName } from '@grafana/ui'; import cn from 'classnames/bind'; -import Text from 'components/Text/Text'; +import Text, { TextType } from 'components/Text/Text'; import styles from './ScheduleCounter.module.css'; interface ScheduleCounterProps { - type: 'link' | 'warning'; + type: Partial; count: number; tooltipTitle: string; tooltipContent: React.ReactNode; @@ -55,8 +55,8 @@ const ScheduleCounter: FC = (props) => { >
- - {count} + + {count}
diff --git a/grafana-plugin/src/components/ScheduleQuality/ScheduleQuality.tsx b/grafana-plugin/src/components/ScheduleQuality/ScheduleQuality.tsx index ac55451551..850ae043de 100644 --- a/grafana-plugin/src/components/ScheduleQuality/ScheduleQuality.tsx +++ b/grafana-plugin/src/components/ScheduleQuality/ScheduleQuality.tsx @@ -20,7 +20,6 @@ const ScheduleQuality: FC = (props) => { }>
- Quality: {Math.floor(quality * 100)}% diff --git a/grafana-plugin/src/components/SchedulesFilters_NEW/SchedulesFilters.types.ts b/grafana-plugin/src/components/SchedulesFilters_NEW/SchedulesFilters.types.ts index ed9cad6269..ec0ab6321b 100644 --- a/grafana-plugin/src/components/SchedulesFilters_NEW/SchedulesFilters.types.ts +++ b/grafana-plugin/src/components/SchedulesFilters_NEW/SchedulesFilters.types.ts @@ -1,13 +1,7 @@ -import { Moment } from 'moment'; - -enum ScheduleType { - Web = 'Web', - iCal = 'iCal', - API = 'API', -} +import { ScheduleType } from 'models/schedule/schedule.types'; export interface SchedulesFiltersType { searchTerm: string; - type: string; + type: ScheduleType; status: string; } diff --git a/grafana-plugin/src/components/Table/Table.tsx b/grafana-plugin/src/components/Table/Table.tsx index 95bcbe8b3c..d639ccf039 100644 --- a/grafana-plugin/src/components/Table/Table.tsx +++ b/grafana-plugin/src/components/Table/Table.tsx @@ -25,7 +25,7 @@ export interface Props extends TableProps { expandable?: { expandedRowKeys: string[]; expandedRowRender: (item: any) => React.ReactNode; - onExpandedRowsChange: (rows: string[]) => void; + onExpandedRowsChange?: (rows: string[]) => void; expandRowByClick: boolean; expandIcon?: (props: { expanded: boolean; record: any }) => React.ReactNode; onExpand?: (expanded: boolean, item: any) => void; diff --git a/grafana-plugin/src/components/Text/Text.tsx b/grafana-plugin/src/components/Text/Text.tsx index 71357fdb76..89f8fdf2ad 100644 --- a/grafana-plugin/src/components/Text/Text.tsx +++ b/grafana-plugin/src/components/Text/Text.tsx @@ -8,8 +8,10 @@ import { openNotification } from 'utils'; import styles from './Text.module.scss'; +export type TextType = 'primary' | 'secondary' | 'disabled' | 'link' | 'success' | 'warning'; + interface TextProps extends HTMLAttributes { - type?: 'primary' | 'secondary' | 'disabled' | 'link' | 'success' | 'warning'; + type?: TextType; strong?: boolean; underline?: boolean; size?: 'small' | 'medium' | 'large'; @@ -24,7 +26,7 @@ interface TextProps extends HTMLAttributes { editModalTitle?: string; } -interface TextType extends React.FC { +interface TextInterface extends React.FC { Title: React.FC; } @@ -32,7 +34,7 @@ const PLACEHOLDER = '**********'; const cx = cn.bind(styles); -const Text: TextType = (props) => { +const Text: TextInterface = (props) => { const { type, size = 'medium', diff --git a/grafana-plugin/src/components/TimelineMarks/TimelineMarks.tsx b/grafana-plugin/src/components/TimelineMarks/TimelineMarks.tsx index a419ba40be..56bf4fe38e 100644 --- a/grafana-plugin/src/components/TimelineMarks/TimelineMarks.tsx +++ b/grafana-plugin/src/components/TimelineMarks/TimelineMarks.tsx @@ -1,7 +1,7 @@ import React, { FC, useMemo } from 'react'; import cn from 'classnames/bind'; -import * as dayjs from 'dayjs'; +import dayjs from 'dayjs'; import styles from './TimelineMarks.module.css'; diff --git a/grafana-plugin/src/components/UserGroups/UserGroups.helpers.ts b/grafana-plugin/src/components/UserGroups/UserGroups.helpers.ts index 2a8b53ce10..dcbfb7bf0b 100644 --- a/grafana-plugin/src/components/UserGroups/UserGroups.helpers.ts +++ b/grafana-plugin/src/components/UserGroups/UserGroups.helpers.ts @@ -1,8 +1,6 @@ -import { Item, ItemData } from './UserGroups.types'; - -export const toPlainArray = (groups: string[][], getItemData: (item: Item['item']) => ItemData) => { - let i = 0; +import { Item } from './UserGroups.types'; +export const toPlainArray = (groups: string[][]) => { const items: Item[] = []; groups.forEach((group: string[], groupIndex: number) => { items.push({ @@ -15,8 +13,7 @@ export const toPlainArray = (groups: string[][], getItemData: (item: Item['item' items.push({ key: `item-${groupIndex}-${itemIndex}`, type: 'item', - item, - data: getItemData(item), + data: item, }); }); }); @@ -25,8 +22,6 @@ export const toPlainArray = (groups: string[][], getItemData: (item: Item['item' }; export const fromPlainArray = (items: Item[], createNewGroup = false, deleteEmptyGroups = true) => { - const groups = []; - return items .reduce((memo: any, item: Item, currentIndex: number) => { if (item.type === 'item') { @@ -35,7 +30,7 @@ export const fromPlainArray = (items: Item[], createNewGroup = false, deleteEmpt lastGroup = []; memo.push(lastGroup); } - lastGroup.push(item.item); + lastGroup.push(item.data); } else { memo.push([]); } diff --git a/grafana-plugin/src/components/UserGroups/UserGroups.tsx b/grafana-plugin/src/components/UserGroups/UserGroups.tsx index cc8d091880..227906834f 100644 --- a/grafana-plugin/src/components/UserGroups/UserGroups.tsx +++ b/grafana-plugin/src/components/UserGroups/UserGroups.tsx @@ -1,20 +1,15 @@ -import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; -import { SelectableValue } from '@grafana/data'; import { VerticalGroup, HorizontalGroup, IconButton, Field, Input } from '@grafana/ui'; import { arrayMoveImmutable } from 'array-move'; import cn from 'classnames/bind'; import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc'; -import Text from 'components/Text/Text'; -import WorkingHours from 'components/WorkingHours/WorkingHours'; -import GSelect from 'containers/GSelect/GSelect'; import RemoteSelect from 'containers/RemoteSelect/RemoteSelect'; -import UserTooltip from 'containers/UserTooltip/UserTooltip'; import { User } from 'models/user/user.types'; import { fromPlainArray, toPlainArray } from './UserGroups.helpers'; -import { Item, ItemData } from './UserGroups.types'; +import { Item } from './UserGroups.types'; import styles from './UserGroups.module.css'; @@ -22,7 +17,6 @@ interface UserGroupsProps { value: Array>; onChange: (value: Array>) => void; isMultipleGroups: boolean; - getItemData: (id: string) => ItemData; renderUser: (id: string) => React.ReactElement; showError?: boolean; } @@ -34,7 +28,7 @@ const DragHandle = () => ; const SortableHandleHoc = SortableHandle(DragHandle); const UserGroups = (props: UserGroupsProps) => { - const { value, onChange, isMultipleGroups, getItemData, renderUser, showError } = props; + const { value, onChange, isMultipleGroups, renderUser, showError } = props; const handleAddUserGroup = useCallback(() => { onChange([...value, []]); @@ -59,7 +53,7 @@ const UserGroups = (props: UserGroupsProps) => { }; const handleUserAdd = useCallback( - (pk: User['pk'], user: User) => { + (pk: User['pk']) => { if (!pk) { return; } @@ -78,7 +72,7 @@ const UserGroups = (props: UserGroupsProps) => { [value] ); - const items = useMemo(() => toPlainArray(value, getItemData), [value]); + const items = useMemo(() => toPlainArray(value), [value]); const onSortEnd = useCallback( ({ oldIndex, newIndex }) => { @@ -97,7 +91,7 @@ const UserGroups = (props: UserGroupsProps) => { const renderItem = (item: Item, index: number) => (
  • - {renderUser(item.item)} + {renderUser(item.data)}
    @@ -140,7 +134,7 @@ interface SortableItemProps { children: React.ReactElement; } -const SortableItem = SortableElement(({ children }: SortableItemProps) => children); +const SortableItem = SortableElement(({ children }) => children); interface SortableListProps { items: Item[]; @@ -150,31 +144,29 @@ interface SortableListProps { renderItem: (item: Item, index: number) => React.ReactElement; } -const SortableList = SortableContainer( - ({ items, handleAddGroup, handleDeleteItem, isMultipleGroups, renderItem }: SortableListProps) => { - return ( -
      - {items.map((item, index) => - item.type === 'item' ? ( - - {renderItem(item, index)} - - ) : isMultipleGroups ? ( - -
    • {item.data.name}
    • -
      - ) : null - )} - {isMultipleGroups && items[items.length - 1]?.type === 'item' && ( - -
    • - Add user group + -
    • +const SortableList = SortableContainer(({ items, handleAddGroup, isMultipleGroups, renderItem }) => { + return ( +
        + {items.map((item, index) => + item.type === 'item' ? ( + + {renderItem(item, index)} + + ) : isMultipleGroups ? ( + +
      • {item.data.name}
      • - )} -
      - ); - } -); + ) : null + )} + {isMultipleGroups && items[items.length - 1]?.type === 'item' && ( + +
    • + Add user group + +
    • +
      + )} +
    + ); +}); export default UserGroups; diff --git a/grafana-plugin/src/components/UserGroups/UserGroups.types.ts b/grafana-plugin/src/components/UserGroups/UserGroups.types.ts index b3b8594222..99d7a3c3ba 100644 --- a/grafana-plugin/src/components/UserGroups/UserGroups.types.ts +++ b/grafana-plugin/src/components/UserGroups/UserGroups.types.ts @@ -2,5 +2,4 @@ export interface Item { key: string; type: string; data: any; - item?: string; } diff --git a/grafana-plugin/src/containers/Rotation/Rotation.tsx b/grafana-plugin/src/containers/Rotation/Rotation.tsx index 7abcdc90c9..0add0728e7 100644 --- a/grafana-plugin/src/containers/Rotation/Rotation.tsx +++ b/grafana-plugin/src/containers/Rotation/Rotation.tsx @@ -120,12 +120,9 @@ const Rotation: FC = (props) => { {events.map((event, index) => { return ( = observer((props) => { const [userGroups, setUserGroups] = useState([[]]); - const getUser = (pk: User['pk']) => { - return { - name: store.userStore.items[pk]?.username, - desc: store.userStore.items[pk]?.timezone, - }; - }; - const renderUser = (userPk: User['pk']) => { const name = store.userStore.items[userPk]?.username; const desc = store.userStore.items[userPk]?.timezone; @@ -274,7 +266,6 @@ const RotationForm: FC = observer((props) => { value={userGroups} onChange={setUserGroups} isMultipleGroups={true} - getItemData={getUser} renderUser={renderUser} showError={!userGroups.some((group) => group.length)} /> @@ -379,7 +370,7 @@ const RotationForm: FC = observer((props) => { Timezone: {getTzOffsetString(dayjs().tz(currentTimezone))} - + {/**/} diff --git a/grafana-plugin/src/containers/RotationForm/ScheduleOverrideForm.tsx b/grafana-plugin/src/containers/RotationForm/ScheduleOverrideForm.tsx index b73373f316..0c213476ae 100644 --- a/grafana-plugin/src/containers/RotationForm/ScheduleOverrideForm.tsx +++ b/grafana-plugin/src/containers/RotationForm/ScheduleOverrideForm.tsx @@ -89,13 +89,6 @@ const ScheduleOverrideForm: FC = (props) => { const [userGroups, setUserGroups] = useState([[]]); - const getUser = (pk: User['pk']) => { - return { - name: store.userStore.items[pk]?.username, - desc: store.userStore.items[pk]?.timezone, - }; - }; - const renderUser = (userPk: User['pk']) => { const name = store.userStore.items[userPk]?.username; const desc = store.userStore.items[userPk]?.timezone; @@ -214,7 +207,6 @@ const ScheduleOverrideForm: FC = (props) => { value={userGroups} onChange={setUserGroups} isMultipleGroups={false} - getItemData={getUser} renderUser={renderUser} showError={!userGroups.some((group) => group.length)} /> diff --git a/grafana-plugin/src/containers/Rotations/Rotations.tsx b/grafana-plugin/src/containers/Rotations/Rotations.tsx index 080dacab35..5702e8cc83 100644 --- a/grafana-plugin/src/containers/Rotations/Rotations.tsx +++ b/grafana-plugin/src/containers/Rotations/Rotations.tsx @@ -1,5 +1,6 @@ import React, { Component, useMemo, useState } from 'react'; +import { SelectableValue } from '@grafana/data'; import { ValuePicker, IconButton, Icon, HorizontalGroup, Button, LoadingPlaceholder } from '@grafana/ui'; import cn from 'classnames/bind'; import dayjs from 'dayjs'; @@ -218,7 +219,7 @@ class Rotations extends Component { this.setState({ shiftIdToShowRotationForm: 'new', layerPriority, shiftMomentToShowRotationForm: moment }); }; - handleAddRotation = (option: SelectOption) => { + handleAddRotation = (option: SelectableValue) => { const { startMoment } = this.props; this.setState({ diff --git a/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx b/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx index a3f367c9bb..cf3f2963f2 100644 --- a/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx +++ b/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx @@ -10,7 +10,7 @@ import { CSSTransition, TransitionGroup } from 'react-transition-group'; import TimelineMarks from 'components/TimelineMarks/TimelineMarks'; import Rotation from 'containers/Rotation/Rotation'; import { getColor, getFromString, getOverrideColor } from 'models/schedule/schedule.helpers'; -import { Layer, Schedule } from 'models/schedule/schedule.types'; +import { Event, Layer, Schedule } from 'models/schedule/schedule.types'; import { Timezone } from 'models/timezone/timezone.types'; import { WithStoreProps } from 'state/types'; import { withMobXProviderContext } from 'state/withStore'; @@ -50,7 +50,10 @@ class ScheduleFinal extends Component); const layers = store.scheduleStore.rotationPreview ? store.scheduleStore.rotationPreview diff --git a/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx b/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx index 2e884c17a7..4e3bd0e0d2 100644 --- a/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx +++ b/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx @@ -11,7 +11,7 @@ import Rotation from 'containers/Rotation/Rotation'; import { RotationCreateData } from 'containers/RotationForm/RotationForm.types'; import ScheduleOverrideForm from 'containers/RotationForm/ScheduleOverrideForm'; import { getFromString, getOverrideColor } from 'models/schedule/schedule.helpers'; -import { Schedule, Shift } from 'models/schedule/schedule.types'; +import { Event, Schedule, Shift } from 'models/schedule/schedule.types'; import { Timezone } from 'models/timezone/timezone.types'; import { WithStoreProps } from 'state/types'; import { withMobXProviderContext } from 'state/withStore'; @@ -50,7 +50,11 @@ class ScheduleOverrides extends Component); const base = 7 * 24 * 60; // in minutes const diff = dayjs().tz(currentTimezone).diff(startMoment, 'minutes'); diff --git a/grafana-plugin/src/containers/ScheduleForm/ScheduleForm.tsx b/grafana-plugin/src/containers/ScheduleForm/ScheduleForm.tsx index e562ec90e1..2c58562ed8 100644 --- a/grafana-plugin/src/containers/ScheduleForm/ScheduleForm.tsx +++ b/grafana-plugin/src/containers/ScheduleForm/ScheduleForm.tsx @@ -24,7 +24,7 @@ interface ScheduleFormProps { id: Schedule['id'] | 'new'; onHide: () => void; onUpdate: () => void; - onCreate: (data: Schedule) => void; + onCreate?: (data: Schedule) => void; type?: ScheduleType; } @@ -63,16 +63,6 @@ const ScheduleForm = observer((props: ScheduleFormProps) => { [id] ); - const getOptionLabel = (item: SelectableValue) => { - const team = grafanaTeamStore.items[item.value]; - return ( - - {item.label} - - - ); - }; - const formConfig = scheduleTypeToForm[data.type]; return ( diff --git a/grafana-plugin/src/models/schedule/schedule.ts b/grafana-plugin/src/models/schedule/schedule.ts index 9d127b9163..bb76853a1a 100644 --- a/grafana-plugin/src/models/schedule/schedule.ts +++ b/grafana-plugin/src/models/schedule/schedule.ts @@ -54,7 +54,7 @@ export class ScheduleStore extends BaseStore { events: { [scheduleId: string]: { [type: string]: { - [startMoment: string]: Array<{ shiftId: string; events: Event[] }> | Layer[]; + [startMoment: string]: Array<{ shiftId: string; events: Event[]; isPreview?: boolean }> | Layer[]; }; }; } = {}; @@ -200,7 +200,7 @@ export class ScheduleStore extends BaseStore { if (isOverride) { this.overridePreview = enrichOverrides( - [...this.events[scheduleId]?.['override']?.[fromString]], + [...(this.events[scheduleId]?.['override']?.[fromString] as Array<{ shiftId: string; events: Event[] }>)], response.rotation, shiftId ); diff --git a/grafana-plugin/src/models/schedule/schedule.types.ts b/grafana-plugin/src/models/schedule/schedule.types.ts index c5a02e4a87..fde7d136a7 100644 --- a/grafana-plugin/src/models/schedule/schedule.types.ts +++ b/grafana-plugin/src/models/schedule/schedule.types.ts @@ -58,8 +58,8 @@ export interface Shift { shift_end: string; shift_start: string; title: string; - type: 2; - until: null; + type: number; + until: string | null; updated_shift: null; } diff --git a/grafana-plugin/src/pages/schedule/Schedule.helpers.ts b/grafana-plugin/src/pages/schedule/Schedule.helpers.ts index 3779a86f36..1e16c5efb4 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.helpers.ts +++ b/grafana-plugin/src/pages/schedule/Schedule.helpers.ts @@ -1,666 +1,8 @@ -import { dateTime, DateTime } from '@grafana/data'; +import { DateTime, dateTime } from '@grafana/data'; import dayjs from 'dayjs'; -import { subtract } from 'lodash-es'; import { Timezone } from 'models/timezone/timezone.types'; -const tzs = [ - 'Africa/Abidjan', - 'Africa/Accra', - 'Africa/Addis_Ababa', - 'Africa/Algiers', - 'Africa/Asmara', - 'Africa/Asmera', - 'Africa/Bamako', - 'Africa/Bangui', - 'Africa/Banjul', - 'Africa/Bissau', - 'Africa/Blantyre', - 'Africa/Brazzaville', - 'Africa/Bujumbura', - 'Africa/Cairo', - 'Africa/Casablanca', - 'Africa/Ceuta', - 'Africa/Conakry', - 'Africa/Dakar', - 'Africa/Dar_es_Salaam', - 'Africa/Djibouti', - 'Africa/Douala', - 'Africa/El_Aaiun', - 'Africa/Freetown', - 'Africa/Gaborone', - 'Africa/Harare', - 'Africa/Johannesburg', - 'Africa/Juba', - 'Africa/Kampala', - 'Africa/Khartoum', - 'Africa/Kigali', - 'Africa/Kinshasa', - 'Africa/Lagos', - 'Africa/Libreville', - 'Africa/Lome', - 'Africa/Luanda', - 'Africa/Lubumbashi', - 'Africa/Lusaka', - 'Africa/Malabo', - 'Africa/Maputo', - 'Africa/Maseru', - 'Africa/Mbabane', - 'Africa/Mogadishu', - 'Africa/Monrovia', - 'Africa/Nairobi', - 'Africa/Ndjamena', - 'Africa/Niamey', - 'Africa/Nouakchott', - 'Africa/Ouagadougou', - 'Africa/Porto-Novo', - 'Africa/Sao_Tome', - 'Africa/Timbuktu', - 'Africa/Tripoli', - 'Africa/Tunis', - 'Africa/Windhoek', - 'America/Adak', - 'America/Anchorage', - 'America/Anguilla', - 'America/Antigua', - 'America/Araguaina', - 'America/Argentina/Buenos_Aires', - 'America/Argentina/Catamarca', - 'America/Argentina/ComodRivadavia', - 'America/Argentina/Cordoba', - 'America/Argentina/Jujuy', - 'America/Argentina/La_Rioja', - 'America/Argentina/Mendoza', - 'America/Argentina/Rio_Gallegos', - 'America/Argentina/Salta', - 'America/Argentina/San_Juan', - 'America/Argentina/San_Luis', - 'America/Argentina/Tucuman', - 'America/Argentina/Ushuaia', - 'America/Aruba', - 'America/Asuncion', - 'America/Atikokan', - 'America/Atka', - 'America/Bahia', - 'America/Bahia_Banderas', - 'America/Barbados', - 'America/Belem', - 'America/Belize', - 'America/Blanc-Sablon', - 'America/Boa_Vista', - 'America/Bogota', - 'America/Boise', - 'America/Buenos_Aires', - 'America/Cambridge_Bay', - 'America/Campo_Grande', - 'America/Cancun', - 'America/Caracas', - 'America/Catamarca', - 'America/Cayenne', - 'America/Cayman', - 'America/Chicago', - 'America/Chihuahua', - 'America/Coral_Harbour', - 'America/Cordoba', - 'America/Costa_Rica', - 'America/Creston', - 'America/Cuiaba', - 'America/Curacao', - 'America/Danmarkshavn', - 'America/Dawson', - 'America/Dawson_Creek', - 'America/Denver', - 'America/Detroit', - 'America/Dominica', - 'America/Edmonton', - 'America/Eirunepe', - 'America/El_Salvador', - 'America/Ensenada', - 'America/Fort_Nelson', - 'America/Fort_Wayne', - 'America/Fortaleza', - 'America/Glace_Bay', - 'America/Godthab', - 'America/Goose_Bay', - 'America/Grand_Turk', - 'America/Grenada', - 'America/Guadeloupe', - 'America/Guatemala', - 'America/Guayaquil', - 'America/Guyana', - 'America/Halifax', - 'America/Havana', - 'America/Hermosillo', - 'America/Indiana/Indianapolis', - 'America/Indiana/Knox', - 'America/Indiana/Marengo', - 'America/Indiana/Petersburg', - 'America/Indiana/Tell_City', - 'America/Indiana/Vevay', - 'America/Indiana/Vincennes', - 'America/Indiana/Winamac', - 'America/Indianapolis', - 'America/Inuvik', - 'America/Iqaluit', - 'America/Jamaica', - 'America/Jujuy', - 'America/Juneau', - 'America/Kentucky/Louisville', - 'America/Kentucky/Monticello', - 'America/Knox_IN', - 'America/Kralendijk', - 'America/La_Paz', - 'America/Lima', - 'America/Los_Angeles', - 'America/Louisville', - 'America/Lower_Princes', - 'America/Maceio', - 'America/Managua', - 'America/Manaus', - 'America/Marigot', - 'America/Martinique', - 'America/Matamoros', - 'America/Mazatlan', - 'America/Mendoza', - 'America/Menominee', - 'America/Merida', - 'America/Metlakatla', - 'America/Mexico_City', - 'America/Miquelon', - 'America/Moncton', - 'America/Monterrey', - 'America/Montevideo', - 'America/Montreal', - 'America/Montserrat', - 'America/Nassau', - 'America/New_York', - 'America/Nipigon', - 'America/Nome', - 'America/Noronha', - 'America/North_Dakota/Beulah', - 'America/North_Dakota/Center', - 'America/North_Dakota/New_Salem', - 'America/Ojinaga', - 'America/Panama', - 'America/Pangnirtung', - 'America/Paramaribo', - 'America/Phoenix', - 'America/Port-au-Prince', - 'America/Port_of_Spain', - 'America/Porto_Acre', - 'America/Porto_Velho', - 'America/Puerto_Rico', - 'America/Punta_Arenas', - 'America/Rainy_River', - 'America/Rankin_Inlet', - 'America/Recife', - 'America/Regina', - 'America/Resolute', - 'America/Rio_Branco', - 'America/Rosario', - 'America/Santa_Isabel', - 'America/Santarem', - 'America/Santiago', - 'America/Santo_Domingo', - 'America/Sao_Paulo', - 'America/Scoresbysund', - 'America/Shiprock', - 'America/Sitka', - 'America/St_Barthelemy', - 'America/St_Johns', - 'America/St_Kitts', - 'America/St_Lucia', - 'America/St_Thomas', - 'America/St_Vincent', - 'America/Swift_Current', - 'America/Tegucigalpa', - 'America/Thule', - 'America/Thunder_Bay', - 'America/Tijuana', - 'America/Toronto', - 'America/Tortola', - 'America/Vancouver', - 'America/Virgin', - 'America/Whitehorse', - 'America/Winnipeg', - 'America/Yakutat', - 'America/Yellowknife', - 'Antarctica/Casey', - 'Antarctica/Davis', - 'Antarctica/DumontDUrville', - 'Antarctica/Macquarie', - 'Antarctica/Mawson', - 'Antarctica/McMurdo', - 'Antarctica/Palmer', - 'Antarctica/Rothera', - 'Antarctica/South_Pole', - 'Antarctica/Syowa', - 'Antarctica/Troll', - 'Antarctica/Vostok', - 'Arctic/Longyearbyen', - 'Asia/Aden', - 'Asia/Almaty', - 'Asia/Amman', - 'Asia/Anadyr', - 'Asia/Aqtau', - 'Asia/Aqtobe', - 'Asia/Ashgabat', - 'Asia/Ashkhabad', - 'Asia/Atyrau', - 'Asia/Baghdad', - 'Asia/Bahrain', - 'Asia/Baku', - 'Asia/Bangkok', - 'Asia/Barnaul', - 'Asia/Beirut', - 'Asia/Bishkek', - 'Asia/Brunei', - 'Asia/Calcutta', - 'Asia/Chita', - 'Asia/Choibalsan', - 'Asia/Chongqing', - 'Asia/Chungking', - 'Asia/Colombo', - 'Asia/Dacca', - 'Asia/Damascus', - 'Asia/Dhaka', - 'Asia/Dili', - 'Asia/Dubai', - 'Asia/Dushanbe', - 'Asia/Famagusta', - 'Asia/Gaza', - 'Asia/Harbin', - 'Asia/Hebron', - 'Asia/Ho_Chi_Minh', - 'Asia/Hong_Kong', - 'Asia/Hovd', - 'Asia/Irkutsk', - 'Asia/Istanbul', - 'Asia/Jakarta', - 'Asia/Jayapura', - 'Asia/Jerusalem', - 'Asia/Kabul', - 'Asia/Kamchatka', - 'Asia/Karachi', - 'Asia/Kashgar', - 'Asia/Kathmandu', - 'Asia/Katmandu', - 'Asia/Khandyga', - 'Asia/Kolkata', - 'Asia/Krasnoyarsk', - 'Asia/Kuala_Lumpur', - 'Asia/Kuching', - 'Asia/Kuwait', - 'Asia/Macao', - 'Asia/Macau', - 'Asia/Magadan', - 'Asia/Makassar', - 'Asia/Manila', - 'Asia/Muscat', - 'Asia/Nicosia', - 'Asia/Novokuznetsk', - 'Asia/Novosibirsk', - 'Asia/Omsk', - 'Asia/Oral', - 'Asia/Phnom_Penh', - 'Asia/Pontianak', - 'Asia/Pyongyang', - 'Asia/Qatar', - 'Asia/Qyzylorda', - 'Asia/Rangoon', - 'Asia/Riyadh', - 'Asia/Saigon', - 'Asia/Sakhalin', - 'Asia/Samarkand', - 'Asia/Seoul', - 'Asia/Shanghai', - 'Asia/Singapore', - 'Asia/Srednekolymsk', - 'Asia/Taipei', - 'Asia/Tashkent', - 'Asia/Tbilisi', - 'Asia/Tehran', - 'Asia/Tel_Aviv', - 'Asia/Thimbu', - 'Asia/Thimphu', - 'Asia/Tokyo', - 'Asia/Tomsk', - 'Asia/Ujung_Pandang', - 'Asia/Ulaanbaatar', - 'Asia/Ulan_Bator', - 'Asia/Urumqi', - 'Asia/Ust-Nera', - 'Asia/Vientiane', - 'Asia/Vladivostok', - 'Asia/Yakutsk', - 'Asia/Yangon', - 'Asia/Yekaterinburg', - 'Asia/Yerevan', - 'Atlantic/Azores', - 'Atlantic/Bermuda', - 'Atlantic/Canary', - 'Atlantic/Cape_Verde', - 'Atlantic/Faeroe', - 'Atlantic/Faroe', - 'Atlantic/Jan_Mayen', - 'Atlantic/Madeira', - 'Atlantic/Reykjavik', - 'Atlantic/South_Georgia', - 'Atlantic/St_Helena', - 'Atlantic/Stanley', - 'Australia/ACT', - 'Australia/Adelaide', - 'Australia/Brisbane', - 'Australia/Broken_Hill', - 'Australia/Canberra', - 'Australia/Currie', - 'Australia/Darwin', - 'Australia/Eucla', - 'Australia/Hobart', - 'Australia/LHI', - 'Australia/Lindeman', - 'Australia/Lord_Howe', - 'Australia/Melbourne', - 'Australia/NSW', - 'Australia/North', - 'Australia/Perth', - 'Australia/Queensland', - 'Australia/South', - 'Australia/Sydney', - 'Australia/Tasmania', - 'Australia/Victoria', - 'Australia/West', - 'Australia/Yancowinna', - 'Brazil/Acre', - 'Brazil/DeNoronha', - 'Brazil/East', - 'Brazil/West', - 'CET', - 'CST6CDT', - 'Canada/Atlantic', - 'Canada/Central', - 'Canada/Eastern', - 'Canada/Mountain', - 'Canada/Newfoundland', - 'Canada/Pacific', - 'Canada/Saskatchewan', - 'Canada/Yukon', - 'Chile/Continental', - 'Chile/EasterIsland', - 'Cuba', - 'EET', - 'EST', - 'EST5EDT', - 'Egypt', - 'Eire', - 'Etc/GMT', - 'Etc/GMT+0', - 'Etc/GMT+1', - 'Etc/GMT+10', - 'Etc/GMT+11', - 'Etc/GMT+12', - 'Etc/GMT+2', - 'Etc/GMT+3', - 'Etc/GMT+4', - 'Etc/GMT+5', - 'Etc/GMT+6', - 'Etc/GMT+7', - 'Etc/GMT+8', - 'Etc/GMT+9', - 'Etc/GMT-0', - 'Etc/GMT-1', - 'Etc/GMT-10', - 'Etc/GMT-11', - 'Etc/GMT-12', - 'Etc/GMT-13', - 'Etc/GMT-14', - 'Etc/GMT-2', - 'Etc/GMT-3', - 'Etc/GMT-4', - 'Etc/GMT-5', - 'Etc/GMT-6', - 'Etc/GMT-7', - 'Etc/GMT-8', - 'Etc/GMT-9', - 'Etc/GMT0', - 'Etc/Greenwich', - 'Etc/UCT', - 'Etc/UTC', - 'Etc/Universal', - 'Etc/Zulu', - 'Europe/Amsterdam', - 'Europe/Andorra', - 'Europe/Astrakhan', - 'Europe/Athens', - 'Europe/Belfast', - 'Europe/Belgrade', - 'Europe/Berlin', - 'Europe/Bratislava', - 'Europe/Brussels', - 'Europe/Bucharest', - 'Europe/Budapest', - 'Europe/Busingen', - 'Europe/Chisinau', - 'Europe/Copenhagen', - 'Europe/Dublin', - 'Europe/Gibraltar', - 'Europe/Guernsey', - 'Europe/Helsinki', - 'Europe/Isle_of_Man', - 'Europe/Istanbul', - 'Europe/Jersey', - 'Europe/Kaliningrad', - 'Europe/Kiev', - 'Europe/Kirov', - 'Europe/Lisbon', - 'Europe/Ljubljana', - 'Europe/London', - 'Europe/Luxembourg', - 'Europe/Madrid', - 'Europe/Malta', - 'Europe/Mariehamn', - 'Europe/Minsk', - 'Europe/Monaco', - 'Europe/Moscow', - 'Europe/Nicosia', - 'Europe/Oslo', - 'Europe/Paris', - 'Europe/Podgorica', - 'Europe/Prague', - 'Europe/Riga', - 'Europe/Rome', - 'Europe/Samara', - 'Europe/San_Marino', - 'Europe/Sarajevo', - 'Europe/Saratov', - 'Europe/Simferopol', - 'Europe/Skopje', - 'Europe/Sofia', - 'Europe/Stockholm', - 'Europe/Tallinn', - 'Europe/Tirane', - 'Europe/Tiraspol', - 'Europe/Ulyanovsk', - 'Europe/Uzhgorod', - 'Europe/Vaduz', - 'Europe/Vatican', - 'Europe/Vienna', - 'Europe/Vilnius', - 'Europe/Volgograd', - 'Europe/Warsaw', - 'Europe/Zagreb', - 'Europe/Zaporozhye', - 'Europe/Zurich', - 'GB', - 'GB-Eire', - 'GMT', - 'GMT+0', - 'GMT-0', - 'GMT0', - 'Greenwich', - 'HST', - 'Hongkong', - 'Iceland', - 'Indian/Antananarivo', - 'Indian/Chagos', - 'Indian/Christmas', - 'Indian/Cocos', - 'Indian/Comoro', - 'Indian/Kerguelen', - 'Indian/Mahe', - 'Indian/Maldives', - 'Indian/Mauritius', - 'Indian/Mayotte', - 'Indian/Reunion', - 'Iran', - 'Israel', - 'Jamaica', - 'Japan', - 'Kwajalein', - 'Libya', - 'MET', - 'MST', - 'MST7MDT', - 'Mexico/BajaNorte', - 'Mexico/BajaSur', - 'Mexico/General', - 'NZ', - 'NZ-CHAT', - 'Navajo', - 'PRC', - 'PST8PDT', - 'Pacific/Apia', - 'Pacific/Auckland', - 'Pacific/Bougainville', - 'Pacific/Chatham', - 'Pacific/Chuuk', - 'Pacific/Easter', - 'Pacific/Efate', - 'Pacific/Enderbury', - 'Pacific/Fakaofo', - 'Pacific/Fiji', - 'Pacific/Funafuti', - 'Pacific/Galapagos', - 'Pacific/Gambier', - 'Pacific/Guadalcanal', - 'Pacific/Guam', - 'Pacific/Honolulu', - 'Pacific/Johnston', - 'Pacific/Kiritimati', - 'Pacific/Kosrae', - 'Pacific/Kwajalein', - 'Pacific/Majuro', - 'Pacific/Marquesas', - 'Pacific/Midway', - 'Pacific/Nauru', - 'Pacific/Niue', - 'Pacific/Norfolk', - 'Pacific/Noumea', - 'Pacific/Pago_Pago', - 'Pacific/Palau', - 'Pacific/Pitcairn', - 'Pacific/Pohnpei', - 'Pacific/Ponape', - 'Pacific/Port_Moresby', - 'Pacific/Rarotonga', - 'Pacific/Saipan', - 'Pacific/Samoa', - 'Pacific/Tahiti', - 'Pacific/Tarawa', - 'Pacific/Tongatapu', - 'Pacific/Truk', - 'Pacific/Wake', - 'Pacific/Wallis', - 'Pacific/Yap', - 'Poland', - 'Portugal', - 'ROC', - 'ROK', - 'Singapore', - 'Turkey', - 'UCT', - 'US/Alaska', - 'US/Aleutian', - 'US/Arizona', - 'US/Central', - 'US/East-Indiana', - 'US/Eastern', - 'US/Hawaii', - 'US/Indiana-Starke', - 'US/Michigan', - 'US/Mountain', - 'US/Pacific', - 'US/Pacific-New', - 'US/Samoa', - 'UTC', - 'Universal', - 'W-SU', - 'WET', - 'Zulu', -]; - -const USERS = [ - 'Innokentii Konstantinov', - 'Ildar Iskhakov', - 'Matias Bordese', - 'Michael Derynck', - 'Vadim Stepanov', - 'Matvey Kukuy', - 'Yulya Artyukhina', - 'Raphael Batyrbaev', -]; - -function getRandomUser() { - return USERS[Math.floor(Math.random() * USERS.length)]; -} - -export const getRandomTimezone = () => { - return tzs[Math.floor(Math.random() * tzs.length)]; -}; - -export const getRandomUsers = (count = 7) => { - const users = []; - for (let i = 0; i < count; i++) { - users.push({ - //name: getRandomUser(), - pk: i, - name: [ - 'Some UTC user', - 'Matias Bordese', - 'Michael Derynck', - 'Yulia Shanyrova', - 'Maxim Mordasov', - 'Vadim Stepanov', - 'Ildar Iskhakov', - /* 'Matvey Kukuy',*/ - ][i], - //avatar: `https://i.pravatar.cc/32?rnd=${Math.random()}`, - avatar: [ - 'https://image.shutterstock.com/image-vector/male-avatar-icon-simple-man-600w-1504887869.jpg', - 'https://avatars.githubusercontent.com/u/260710?v=4', - 'https://avatars.githubusercontent.com/u/28077050?s=60&v=4', - 'https://avatars.githubusercontent.com/u/20494436?v=4', - 'https://avatars.githubusercontent.com/u/3278022?v=4', - 'https://avatars.githubusercontent.com/u/20116910?s=60&v=4', - 'https://avatars.githubusercontent.com/u/2262529?v=4', - ][i], - //tz: getRandomTimezone(), - tz: [ - 'UTC', - 'America/Montevideo', - 'America/Vancouver', - 'Europe/Amsterdam', - 'Europe/Moscow', - 'Europe/London', - 'Asia/Yerevan', - /*'Asia/Tel_Aviv',*/ - ][i], - }); - } - - return users; -}; - export const getStartOfWeek = (tz: Timezone) => { return dayjs().tz(tz).utcOffset() === 0 ? dayjs().utc().startOf('isoWeek') : dayjs().tz(tz).startOf('isoWeek'); }; @@ -671,7 +13,7 @@ export const getUTCString = (moment: dayjs.Dayjs | DateTime, timezone: Timezone) const browserTimezoneOffset = dayjs().tz(browserTimezone).utcOffset(); const timezoneOffset = dayjs().tz(timezone).utcOffset(); - return moment + return (moment as dayjs.Dayjs) .clone() .utc() .add(browserTimezoneOffset, 'minutes') // we need these calculations because we can't specify timezone for DateTimePicker directly diff --git a/grafana-plugin/src/pages/schedule/Schedule.tsx b/grafana-plugin/src/pages/schedule/Schedule.tsx index 0cdbc4dabd..2410ada8c4 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.tsx +++ b/grafana-plugin/src/pages/schedule/Schedule.tsx @@ -21,12 +21,10 @@ import ScheduleFinal from 'containers/Rotations/ScheduleFinal'; import ScheduleOverrides from 'containers/Rotations/ScheduleOverrides'; import UsersTimezones from 'containers/UsersTimezones/UsersTimezones'; import { Timezone } from 'models/timezone/timezone.types'; -import { User } from 'models/user/user.types'; -import { AppFeature } from 'state/features'; import { WithStoreProps } from 'state/types'; import { withMobXProviderContext } from 'state/withStore'; -import { getRandomUsers, getStartOfWeek, getUTCString } from './Schedule.helpers'; +import { getStartOfWeek, getUTCString } from './Schedule.helpers'; import styles from './Schedule.module.css'; @@ -125,8 +123,8 @@ class SchedulePage extends React.Component {users && ( )} - {/* - + + {/* */} diff --git a/grafana-plugin/src/pages/schedules_NEW/Schedules.helpers.ts b/grafana-plugin/src/pages/schedules_NEW/Schedules.helpers.ts deleted file mode 100644 index bedc40230e..0000000000 --- a/grafana-plugin/src/pages/schedules_NEW/Schedules.helpers.ts +++ /dev/null @@ -1,42 +0,0 @@ -import dayjs from 'dayjs'; - -import { getColor, getOverrideColor, getRandomUser } from 'components/Rotations/Rotations.helpers'; -import { getRandomUsers } from 'pages/schedule/Schedule.helpers'; - -export const getRandomSchedules = () => { - const schedules = []; - for (let i = 0; i < 20; i++) { - schedules.push({ - id: i + 1, - name: `Schedule Team ${i + 1}`, - users: getRandomUsers(2), - chatOps: '#irm-incidents-primary', - quality: 20 + Math.floor(Math.random() * 80), - }); - } - - return schedules; -}; - -export const getRandomTimeslots = (count = 6) => { - const slots = []; - for (let i = 0; i < count; i++) { - const start = dayjs() - .startOf('day') - .add(i * 4, 'hour'); - const end = dayjs() - .startOf('day') - .add(i * 4 + 2, 'hour'); - //const inactive = end.isBefore(dayjs()); - const inactive = false; - - slots.push({ - start, - end, - inactive, - users: [getRandomUser()], - color: getOverrideColor(i), - }); - } - return slots; -}; diff --git a/grafana-plugin/src/pages/schedules_NEW/Schedules.tsx b/grafana-plugin/src/pages/schedules_NEW/Schedules.tsx index f9a76c8962..417761a0a2 100644 --- a/grafana-plugin/src/pages/schedules_NEW/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules_NEW/Schedules.tsx @@ -153,7 +153,6 @@ class SchedulesPage extends React.Component cx('expanded-row'), }} emptyText={
    @@ -306,15 +305,15 @@ class SchedulesPage extends React.Component { + /* renderChatOps = (item: Schedule) => { return item.chatOps; - }; + }; */ - renderQuality = (item: Schedule) => { + /* renderQuality = (item: Schedule) => { const type = item.quality > 70 ? 'primary' : 'warning'; return {item.quality || 70}%; - }; + }; */ renderButtons = (item: Schedule) => { return ( diff --git a/grafana-plugin/tsconfig.json b/grafana-plugin/tsconfig.json index 5f72aa4373..add73ef907 100644 --- a/grafana-plugin/tsconfig.json +++ b/grafana-plugin/tsconfig.json @@ -1,9 +1,9 @@ { "extends": "@grafana/toolkit/src/config/tsconfig.plugin.json", - "include": ["src/dummy"], + "include": ["src", "frontend_enterprise/src"], "types": ["node", "@emotion/core"], "compilerOptions": { - "rootDirs": ["src"], + "rootDirs": ["src", "frontend_enterprise/src"], "baseUrl": "src", "typeRoots": ["./node_modules/@types"], "noUnusedLocals": false, From 284fcfe74d29b14329914ac92b62354f12c2ab81 Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Tue, 13 Sep 2022 10:30:34 -0300 Subject: [PATCH 10/28] Refactoring, adding tests --- engine/apps/schedules/ical_utils.py | 4 +-- .../apps/schedules/tests/test_ical_utils.py | 32 ++++++++++++++++++- .../schedules/tests/test_on_call_schedule.py | 32 ++++++++++++++++++- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/engine/apps/schedules/ical_utils.py b/engine/apps/schedules/ical_utils.py index 09a0b3072f..41af773137 100644 --- a/engine/apps/schedules/ical_utils.py +++ b/engine/apps/schedules/ical_utils.py @@ -134,7 +134,7 @@ def list_of_oncall_shifts_from_ical( continue tmp_result_datetime, tmp_result_date = get_shifts_dict( - calendar, calendar_type, schedule, datetime_start, datetime_end, date, with_empty_shifts + calendar, calendar_type, schedule, datetime_start, datetime_end, with_empty_shifts ) result_datetime.extend(tmp_result_datetime) result_date.extend(tmp_result_date) @@ -161,7 +161,7 @@ def list_of_oncall_shifts_from_ical( return result or None -def get_shifts_dict(calendar, calendar_type, schedule, datetime_start, datetime_end, date, with_empty_shifts=False): +def get_shifts_dict(calendar, calendar_type, schedule, datetime_start, datetime_end, with_empty_shifts=False): events = ical_events.get_events_from_ical_between(calendar, datetime_start, datetime_end) result_datetime = [] result_date = [] diff --git a/engine/apps/schedules/tests/test_ical_utils.py b/engine/apps/schedules/tests/test_ical_utils.py index 38213be80d..f6bc21555b 100644 --- a/engine/apps/schedules/tests/test_ical_utils.py +++ b/engine/apps/schedules/tests/test_ical_utils.py @@ -1,9 +1,16 @@ +import datetime from uuid import uuid4 import pytest +import pytz from django.utils import timezone -from apps.schedules.ical_utils import list_users_to_notify_from_ical, parse_event_uid, users_in_ical +from apps.schedules.ical_utils import ( + list_of_oncall_shifts_from_ical, + list_users_to_notify_from_ical, + parse_event_uid, + users_in_ical, +) from apps.schedules.models import CustomOnCallShift, OnCallScheduleCalendar from common.constants.role import Role @@ -63,6 +70,29 @@ def test_list_users_to_notify_from_ical_viewers_inclusion( assert set(users_on_call) == {user} +@pytest.mark.django_db +def test_shifts_dict_all_day_middle_event(make_organization, make_schedule, get_ical): + calendar = get_ical("calendar_with_all_day_event.ics") + organization = make_organization() + schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar) + schedule.cached_ical_file_primary = calendar.to_ical() + + day_to_check_iso = "2021-01-27T15:27:14.448059+00:00" + parsed_iso_day_to_check = datetime.datetime.fromisoformat(day_to_check_iso).replace(tzinfo=pytz.UTC) + requested_date = (parsed_iso_day_to_check - timezone.timedelta(days=1)).date() + shifts = list_of_oncall_shifts_from_ical(schedule, requested_date, days=3, with_empty_shifts=True) + assert len(shifts) == 4 + for s in shifts: + start = s["start"].date() if isinstance(s["start"], datetime.datetime) else s["start"] + end = s["end"].date() if isinstance(s["end"], datetime.datetime) else s["end"] + # event started in the given period, or ended in that period, or is happening during the period + assert ( + requested_date <= start <= requested_date + timezone.timedelta(days=3) + or requested_date <= end <= requested_date + timezone.timedelta(days=3) + or start <= requested_date <= end + ) + + def test_parse_event_uid_v1(): uuid = uuid4() event_uid = f"amixr-{uuid}-U1-E2-S1" diff --git a/engine/apps/schedules/tests/test_on_call_schedule.py b/engine/apps/schedules/tests/test_on_call_schedule.py index f46bb4b29e..e8da0e67ec 100644 --- a/engine/apps/schedules/tests/test_on_call_schedule.py +++ b/engine/apps/schedules/tests/test_on_call_schedule.py @@ -1,7 +1,10 @@ +import datetime + import pytest +import pytz from django.utils import timezone -from apps.schedules.models import CustomOnCallShift, OnCallSchedule, OnCallScheduleWeb +from apps.schedules.models import CustomOnCallShift, OnCallSchedule, OnCallScheduleCalendar, OnCallScheduleWeb from common.constants.role import Role @@ -225,6 +228,33 @@ def test_filter_events_include_empty(make_organization, make_user_for_organizati assert events == expected +@pytest.mark.django_db +def test_filter_events_ical_all_day(make_organization, make_user_for_organization, make_schedule, get_ical): + calendar = get_ical("calendar_with_all_day_event.ics") + organization = make_organization() + schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar) + schedule.cached_ical_file_primary = calendar.to_ical() + for u in ("@Bernard Desruisseaux", "@Bob", "@Alex"): + make_user_for_organization(organization, username=u) + + day_to_check_iso = "2021-01-27T15:27:14.448059+00:00" + parsed_iso_day_to_check = datetime.datetime.fromisoformat(day_to_check_iso).replace(tzinfo=pytz.UTC) + start_date = (parsed_iso_day_to_check - timezone.timedelta(days=1)).date() + + events = schedule.final_events("UTC", start_date, days=2) + expected_events = [ + # all_day, users, start + (False, ["@Bernard Desruisseaux"], datetime.datetime(2021, 1, 26, 8, 0, tzinfo=pytz.UTC)), + (True, ["@Alex"], datetime.date(2021, 1, 27)), + (False, ["@Bob"], datetime.datetime(2021, 1, 27, 8, 0, tzinfo=pytz.UTC)), + ] + expected = [{"all_day": all_day, "users": users, "start": start} for all_day, users, start in expected_events] + returned = [ + {"all_day": e["all_day"], "users": [u["display_name"] for u in e["users"]], "start": e["start"]} for e in events + ] + assert returned == expected + + @pytest.mark.django_db def test_final_schedule_events(make_organization, make_user_for_organization, make_on_call_shift, make_schedule): organization = make_organization() From 4dfabb5ead1c71e7e31dbbc71c7e61a9d36cf273 Mon Sep 17 00:00:00 2001 From: "S. M. Mir-Ismaili" Date: Wed, 14 Sep 2022 16:48:32 +0430 Subject: [PATCH 11/28] Increase num of `getPluginSyncStatus` retries to 10 --- grafana-plugin/src/state/rootBaseStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grafana-plugin/src/state/rootBaseStore.ts b/grafana-plugin/src/state/rootBaseStore.ts index e68a701b81..7cc2641609 100644 --- a/grafana-plugin/src/state/rootBaseStore.ts +++ b/grafana-plugin/src/state/rootBaseStore.ts @@ -219,7 +219,7 @@ export class RootBaseStore { this.handleSyncException(e); }); - if (counter >= 5) { + if (counter >= 10) { clearInterval(interval); this.retrySync = true; } From c06d021906437267abd694ba76582905bbaafe10 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 14 Sep 2022 15:35:39 +0300 Subject: [PATCH 12/28] ux for token form --- .../SourceCode/SourceCode.module.scss | 15 ++++++------ .../src/components/SourceCode/SourceCode.tsx | 24 ++++++++++--------- .../ApiTokenSettings/ApiTokenForm.module.css | 5 +++- .../ApiTokenSettings/ApiTokenForm.tsx | 2 +- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/grafana-plugin/src/components/SourceCode/SourceCode.module.scss b/grafana-plugin/src/components/SourceCode/SourceCode.module.scss index a926c09d27..82123f27fc 100644 --- a/grafana-plugin/src/components/SourceCode/SourceCode.module.scss +++ b/grafana-plugin/src/components/SourceCode/SourceCode.module.scss @@ -2,28 +2,27 @@ position: relative; width: 100%; - &:hover .button { + &:hover .copyButton, + &:hover .copyIcon { opacity: 1; } } .scroller { overflow-y: auto; + border-radius: 2px; + padding: 12px 60px 12px 20px; &--maxHeight { max-height: 400px; } } -.button { +.copyIcon, +.copyButton { position: absolute; top: 15px; right: 15px; opacity: 0; transition: opacity 0.2s ease; -} - -pre { - border-radius: 2px; - padding: 12px 20px; -} +} \ No newline at end of file diff --git a/grafana-plugin/src/components/SourceCode/SourceCode.tsx b/grafana-plugin/src/components/SourceCode/SourceCode.tsx index da4f8838d0..f2f350214d 100644 --- a/grafana-plugin/src/components/SourceCode/SourceCode.tsx +++ b/grafana-plugin/src/components/SourceCode/SourceCode.tsx @@ -1,41 +1,43 @@ import React, { FC } from 'react'; -import { Button } from '@grafana/ui'; +import { Button, Icon, IconButton } from '@grafana/ui'; import cn from 'classnames/bind'; import CopyToClipboard from 'react-copy-to-clipboard'; import { openNotification } from 'utils'; import styles from './SourceCode.module.scss'; +import Text from 'components/Text/Text'; const cx = cn.bind(styles); interface SourceCodeProps { noMaxHeight?: boolean; + showClipboardIconOnly?: boolean; showCopyToClipboard?: boolean; children?: any; } const SourceCode: FC = (props) => { - const { children, noMaxHeight = false, showCopyToClipboard = true } = props; + const { children, noMaxHeight = false, showClipboardIconOnly = false, showCopyToClipboard = true } = props; + const showClipboardCopy = showClipboardIconOnly || showCopyToClipboard; return (
    - {showCopyToClipboard && ( + {showClipboardCopy && ( { openNotification('Copied!'); }} > - + {showClipboardIconOnly ? ( + + ) : ( + + )} )}
     {
         return (
           
             
    -        {getCurlExample(token)}
    +        {getCurlExample(token)}
           
         );
       }
    
    From 18adabd9edda691d2464cd481a7959c07d942d8c Mon Sep 17 00:00:00 2001
    From: Rares Mardare 
    Date: Wed, 14 Sep 2022 15:42:14 +0300
    Subject: [PATCH 13/28] linter
    
    ---
     .../src/components/SourceCode/SourceCode.module.scss          | 4 +++-
     grafana-plugin/src/components/SourceCode/SourceCode.tsx       | 1 -
     2 files changed, 3 insertions(+), 2 deletions(-)
    
    diff --git a/grafana-plugin/src/components/SourceCode/SourceCode.module.scss b/grafana-plugin/src/components/SourceCode/SourceCode.module.scss
    index 82123f27fc..5dddbcdf84 100644
    --- a/grafana-plugin/src/components/SourceCode/SourceCode.module.scss
    +++ b/grafana-plugin/src/components/SourceCode/SourceCode.module.scss
    @@ -23,6 +23,8 @@
       position: absolute;
       top: 15px;
       right: 15px;
    -  opacity: 0;
       transition: opacity 0.2s ease;
    +}
    +.copyButton {
    +  opacity: 0;
     }
    \ No newline at end of file
    diff --git a/grafana-plugin/src/components/SourceCode/SourceCode.tsx b/grafana-plugin/src/components/SourceCode/SourceCode.tsx
    index f2f350214d..de97190c43 100644
    --- a/grafana-plugin/src/components/SourceCode/SourceCode.tsx
    +++ b/grafana-plugin/src/components/SourceCode/SourceCode.tsx
    @@ -7,7 +7,6 @@ import CopyToClipboard from 'react-copy-to-clipboard';
     import { openNotification } from 'utils';
     
     import styles from './SourceCode.module.scss';
    -import Text from 'components/Text/Text';
     
     const cx = cn.bind(styles);
     
    
    From fd38af4e1797fb5decc3461eb161032f5747342a Mon Sep 17 00:00:00 2001
    From: Rares Mardare 
    Date: Wed, 14 Sep 2022 16:01:50 +0300
    Subject: [PATCH 14/28] unused css rule
    
    ---
     .../src/components/SourceCode/SourceCode.module.scss           | 3 +--
     1 file changed, 1 insertion(+), 2 deletions(-)
    
    diff --git a/grafana-plugin/src/components/SourceCode/SourceCode.module.scss b/grafana-plugin/src/components/SourceCode/SourceCode.module.scss
    index 5dddbcdf84..0b281c0508 100644
    --- a/grafana-plugin/src/components/SourceCode/SourceCode.module.scss
    +++ b/grafana-plugin/src/components/SourceCode/SourceCode.module.scss
    @@ -2,8 +2,7 @@
       position: relative;
       width: 100%;
     
    -  &:hover .copyButton,
    -  &:hover .copyIcon {
    +  &:hover .copyButton {
         opacity: 1;
       }
     }
    
    From 77984b77ab3cb690e7729a9d5c2586834fd0be4f Mon Sep 17 00:00:00 2001
    From: Vadim Stepanov 
    Date: Fri, 16 Sep 2022 12:36:50 +0100
    Subject: [PATCH 15/28] Fix empty byday in ICAL rrule (#535)
    
    ---
     engine/apps/schedules/models/custom_on_call_shift.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/engine/apps/schedules/models/custom_on_call_shift.py b/engine/apps/schedules/models/custom_on_call_shift.py
    index 1728458d9f..c4558b12a4 100644
    --- a/engine/apps/schedules/models/custom_on_call_shift.py
    +++ b/engine/apps/schedules/models/custom_on_call_shift.py
    @@ -441,7 +441,7 @@ def event_ical_rules(self):
                 rules["freq"] = [self.get_frequency_display().upper()]
                 if self.event_interval is not None:
                     rules["interval"] = [self.event_interval]
    -            if self.by_day is not None:
    +            if self.by_day:
                     rules["byday"] = self.by_day
                 if self.by_month is not None:
                     rules["bymonth"] = self.by_month
    
    From 9c0b30bde2e15de589da446af0d5a939e9240641 Mon Sep 17 00:00:00 2001
    From: Rares Mardare 
    Date: Fri, 16 Sep 2022 16:02:51 +0300
    Subject: [PATCH 16/28] change api endpoint
    
    ---
     grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.tsx | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.tsx b/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.tsx
    index 5457d4f698..f0f98b74c8 100644
    --- a/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.tsx
    +++ b/grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.tsx
    @@ -107,7 +107,7 @@ const ApiTokenForm = observer((props: TokenCreationModalProps) => {
     });
     
     function getCurlExample(token) {
    -  return `curl -H "Authorization: ${token}" ${getItem('onCallApiUrl')}/api/internal/v1/alert_receive_channels`;
    +  return `curl -H "Authorization: ${token}" ${getItem('onCallApiUrl')}/api/v1/integrations`;
     }
     
     export default ApiTokenForm;
    
    From 9330b891014055672206439ddfb836399d95a6de Mon Sep 17 00:00:00 2001
    From: Maxim Mordasov 
    Date: Wed, 21 Sep 2022 13:19:59 +0300
    Subject: [PATCH 17/28] Schedules alpha fixes (#541)
    
    * schedule alpha major fixes
    
    * Fix shift update for web schedules
    
    * Fix priority level regex, fix getting shifts without duration
    
    * Fix shift update for web schedules
    
    * Fix tests for shift update
    
    * Fix priority level test
    
    * schedule alpha fixes
    
    * add final schedule click handler
    
    * fix date time picker
    
    * fix utc timzeonr time picker
    
    * fix utc time data
    
    * dont use user timezone on start
    
    Co-authored-by: Julia 
    ---
     engine/apps/api/tests/test_oncall_shift.py    |  28 +-
     engine/apps/schedules/constants.py            |   2 +-
     engine/apps/schedules/ical_utils.py           |  25 +-
     .../schedules/models/custom_on_call_shift.py  |  42 ++-
     .../slack/tests/test_parse_slack_usernames.py |   2 +-
     grafana-plugin/src/components/Modal/Modal.tsx |   1 +
     .../src/components/Text/Text.module.scss      |   6 +
     .../TimelineMarks/TimelineMarks.tsx           |   2 +-
     .../src/components/UserGroups/UserGroups.tsx  |  19 +-
     .../UserTimezoneSelect/UserTimezoneSelect.tsx |  71 +++--
     .../components/WorkingHours/WorkingHours.tsx  |   2 +-
     .../containers/RemoteSelect/RemoteSelect.tsx  |   3 +
     .../containers/Rotation/Rotation.module.css   |   2 +-
     .../RotationForm/DateTimePicker.tsx           |  89 ++++++
     .../RotationForm/RotationForm.module.css      |  23 +-
     .../containers/RotationForm/RotationForm.tsx  | 260 ++++++++++--------
     .../RotationForm/ScheduleOverrideForm.tsx     | 112 ++++----
     .../src/containers/Rotations/Rotations.tsx    |  63 +++--
     .../containers/Rotations/ScheduleFinal.tsx    |  13 +-
     .../Rotations/ScheduleOverrides.tsx           |  33 ++-
     .../UsersTimezones/UsersTimezones.tsx         |  16 +-
     .../src/models/schedule/schedule.ts           |   2 +
     .../src/models/schedule/schedule.types.ts     |   2 +-
     grafana-plugin/src/models/user/user.ts        |   6 +-
     .../src/pages/schedule/Schedule.helpers.ts    |  28 +-
     .../src/pages/schedule/Schedule.tsx           |  65 ++++-
     .../src/pages/schedules_NEW/Schedules.tsx     |   7 +
     grafana-plugin/src/state/rootBaseStore.ts     |   3 +-
     28 files changed, 596 insertions(+), 331 deletions(-)
     create mode 100644 grafana-plugin/src/containers/RotationForm/DateTimePicker.tsx
    
    diff --git a/engine/apps/api/tests/test_oncall_shift.py b/engine/apps/api/tests/test_oncall_shift.py
    index ddd2149865..efa2fb9667 100644
    --- a/engine/apps/api/tests/test_oncall_shift.py
    +++ b/engine/apps/api/tests/test_oncall_shift.py
    @@ -412,8 +412,9 @@ def test_update_old_on_call_shift_with_future_version(
         token, user1, user2, organization, schedule = on_call_shift_internal_api_setup
     
         client = APIClient()
    -    start_date = (timezone.now() - timezone.timedelta(days=3)).replace(microsecond=0)
    -    next_rotation_start_date = start_date + timezone.timedelta(days=5)
    +    now = timezone.now().replace(microsecond=0)
    +    start_date = now - timezone.timedelta(days=3)
    +    next_rotation_start_date = now + timezone.timedelta(days=1)
         updated_duration = timezone.timedelta(hours=4)
     
         title = "Test Shift Rotation"
    @@ -422,10 +423,11 @@ def test_update_old_on_call_shift_with_future_version(
             shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT,
             schedule=schedule,
             title=title,
    -        start=start_date,
    +        start=next_rotation_start_date,
             duration=timezone.timedelta(hours=3),
             rotation_start=next_rotation_start_date,
             rolling_users=[{user1.pk: user1.public_primary_key}],
    +        frequency=CustomOnCallShift.FREQUENCY_DAILY,
         )
         old_on_call_shift = make_on_call_shift(
             schedule.organization,
    @@ -438,6 +440,7 @@ def test_update_old_on_call_shift_with_future_version(
             until=next_rotation_start_date,
             rolling_users=[{user1.pk: user1.public_primary_key}],
             updated_shift=new_on_call_shift,
    +        frequency=CustomOnCallShift.FREQUENCY_DAILY,
         )
         # update shift_end and priority_level
         data_to_update = {
    @@ -445,9 +448,9 @@ def test_update_old_on_call_shift_with_future_version(
             "priority_level": 2,
             "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"),
             "shift_end": (start_date + updated_duration).strftime("%Y-%m-%dT%H:%M:%SZ"),
    -        "rotation_start": next_rotation_start_date.strftime("%Y-%m-%dT%H:%M:%SZ"),
    +        "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"),
             "until": None,
    -        "frequency": None,
    +        "frequency": CustomOnCallShift.FREQUENCY_DAILY,
             "interval": None,
             "by_day": None,
             "rolling_users": [[user1.public_primary_key]],
    @@ -461,27 +464,28 @@ def test_update_old_on_call_shift_with_future_version(
         url = reverse("api-internal:oncall_shifts-detail", kwargs={"pk": old_on_call_shift.public_primary_key})
     
         response = client.put(url, data=data_to_update, format="json", **make_user_auth_headers(user1, token))
    +    response_data = response.json()
     
    -    next_shift_start_date = timezone.datetime.combine(next_rotation_start_date.date(), start_date.time()).astimezone(
    -        timezone.pytz.UTC
    -    )
    +    for key in ["shift_start", "shift_end", "rotation_start"]:
    +        data_to_update.pop(key)
    +        response_data.pop(key)
     
         expected_payload = data_to_update | {
             "id": new_on_call_shift.public_primary_key,
             "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT,
             "schedule": schedule.public_primary_key,
             "updated_shift": None,
    -        "shift_start": next_shift_start_date.strftime("%Y-%m-%dT%H:%M:%SZ"),
    -        "shift_end": (next_shift_start_date + updated_duration).strftime("%Y-%m-%dT%H:%M:%SZ"),
         }
     
         assert response.status_code == status.HTTP_200_OK
         assert response.json() == expected_payload
     
    -    new_on_call_shift.refresh_from_db()
    -    # check if the newest version of shift was changed
         assert old_on_call_shift.duration != updated_duration
         assert old_on_call_shift.priority_level != data_to_update["priority_level"]
    +    new_on_call_shift.refresh_from_db()
    +    # check if the newest version of shift was changed
    +    assert new_on_call_shift.start - now < timezone.timedelta(minutes=1)
    +    assert new_on_call_shift.rotation_start - now < timezone.timedelta(minutes=1)
         assert new_on_call_shift.duration == updated_duration
         assert new_on_call_shift.priority_level == data_to_update["priority_level"]
     
    diff --git a/engine/apps/schedules/constants.py b/engine/apps/schedules/constants.py
    index 719aa0b219..a2ec8adc8a 100644
    --- a/engine/apps/schedules/constants.py
    +++ b/engine/apps/schedules/constants.py
    @@ -9,6 +9,6 @@
     ICAL_UID = "UID"
     ICAL_RRULE = "RRULE"
     ICAL_UNTIL = "UNTIL"
    -RE_PRIORITY = re.compile(r"^\[L(\d)\]")
    +RE_PRIORITY = re.compile(r"^\[L(\d+)\]")
     RE_EVENT_UID_V1 = re.compile(r"amixr-([\w\d-]+)-U(\d+)-E(\d+)-S(\d+)")
     RE_EVENT_UID_V2 = re.compile(r"oncall-([\w\d-]+)-PK([\w\d]+)-U(\d+)-E(\d+)-S(\d+)")
    diff --git a/engine/apps/schedules/ical_utils.py b/engine/apps/schedules/ical_utils.py
    index 12fb5bd1bd..620e4450d8 100644
    --- a/engine/apps/schedules/ical_utils.py
    +++ b/engine/apps/schedules/ical_utils.py
    @@ -190,18 +190,19 @@ def get_shifts_dict(calendar, calendar_type, schedule, datetime_start, datetime_
                         )
                 else:
                     start, end = ical_events.get_start_and_end_with_respect_to_event_type(event)
    -                result_datetime.append(
    -                    {
    -                        "start": start.astimezone(pytz.UTC),
    -                        "end": end.astimezone(pytz.UTC),
    -                        "users": users,
    -                        "missing_users": missing_users,
    -                        "priority": priority,
    -                        "source": source,
    -                        "calendar_type": calendar_type,
    -                        "shift_pk": pk,
    -                    }
    -                )
    +                if start < end:
    +                    result_datetime.append(
    +                        {
    +                            "start": start.astimezone(pytz.UTC),
    +                            "end": end.astimezone(pytz.UTC),
    +                            "users": users,
    +                            "missing_users": missing_users,
    +                            "priority": priority,
    +                            "source": source,
    +                            "calendar_type": calendar_type,
    +                            "shift_pk": pk,
    +                        }
    +                    )
         return result_datetime, result_date
     
     
    diff --git a/engine/apps/schedules/models/custom_on_call_shift.py b/engine/apps/schedules/models/custom_on_call_shift.py
    index c4558b12a4..232c824a40 100644
    --- a/engine/apps/schedules/models/custom_on_call_shift.py
    +++ b/engine/apps/schedules/models/custom_on_call_shift.py
    @@ -433,6 +433,34 @@ def get_rotation_date(self, event_ical, get_next_date=False):
                 return
             return next_event_dt
     
    +    def get_last_event_date(self, date):
    +        """Get start date of the last event before the chosen date"""
    +        assert date >= self.start, "Chosen date should be later or equal to initial event start date"
    +
    +        event_ical = self.generate_ical(self.start)
    +        initial_event = Event.from_ical(event_ical)
    +        # take shift interval, not event interval. For rolling_users shift it is not the same.
    +        interval = self.interval or 1
    +        if "rrule" in initial_event:
    +            # means that shift has frequency
    +            initial_event["rrule"]["INTERVAL"] = interval
    +        initial_event_start = initial_event["DTSTART"].dt
    +
    +        last_event = None
    +        # repetitions generate the next event shift according with the recurrence rules
    +        repetitions = UnfoldableCalendar(initial_event).RepeatedEvent(
    +            initial_event, initial_event_start.replace(microsecond=0)
    +        )
    +        ical_iter = repetitions.__iter__()
    +        for event in ical_iter:
    +            if event.start > date:
    +                break
    +            last_event = event
    +
    +        last_event_dt = last_event.start if last_event else initial_event_start
    +
    +        return last_event_dt
    +
         @cached_property
         def event_ical_rules(self):
             # e.g. {'freq': ['WEEKLY'], 'interval': [2], 'byday': ['MO', 'WE', 'FR'], 'wkst': ['SU']}
    @@ -498,10 +526,9 @@ def add_rolling_users(self, rolling_users_list):
             self.rolling_users = result
             self.save(update_fields=["rolling_users"])
     
    -    def get_rotation_user_index(self, date=None):
    +    def get_rotation_user_index(self, date):
             START_ROTATION_INDEX = 0
     
    -        date = timezone.now() if not date else date
             result = START_ROTATION_INDEX
     
             if not self.rolling_users or self.frequency is None:
    @@ -544,8 +571,9 @@ def last_updated_shift(self):
             return last_shift
     
         def create_or_update_last_shift(self, data):
    +        now = timezone.now().replace(microsecond=0)
             # rotation start date cannot be earlier than now
    -        data["rotation_start"] = max(data["rotation_start"], timezone.now().replace(microsecond=0))
    +        data["rotation_start"] = max(data["rotation_start"], now)
             # prepare dict with params of existing instance with last updates and remove unique and m2m fields from it
             shift_to_update = self.last_updated_shift or self
             instance_data = model_to_dict(shift_to_update)
    @@ -557,12 +585,10 @@ def create_or_update_last_shift(self, data):
             instance_data["schedule"] = self.schedule
             instance_data["team"] = self.team
             # set new event start date to keep rotation index
    -        instance_data["start"] = timezone.datetime.combine(
    -            instance_data["rotation_start"].date(),
    -            instance_data["start"].time(),
    -        ).astimezone(pytz.UTC)
    +        if instance_data["start"] == self.start:
    +            instance_data["start"] = self.get_last_event_date(now)
             # calculate rotation index to keep user rotation order
    -        start_rotation_from_user_index = self.get_rotation_user_index() + (self.start_rotation_from_user_index or 0)
    +        start_rotation_from_user_index = self.get_rotation_user_index(now) + (self.start_rotation_from_user_index or 0)
             if start_rotation_from_user_index >= len(instance_data["rolling_users"]):
                 start_rotation_from_user_index = 0
             instance_data["start_rotation_from_user_index"] = start_rotation_from_user_index
    diff --git a/engine/apps/slack/tests/test_parse_slack_usernames.py b/engine/apps/slack/tests/test_parse_slack_usernames.py
    index df43c950a2..659cc27a66 100644
    --- a/engine/apps/slack/tests/test_parse_slack_usernames.py
    +++ b/engine/apps/slack/tests/test_parse_slack_usernames.py
    @@ -52,5 +52,5 @@ def test_remove_priority_from_username():
         assert parse_username_from_string("[L1] bob") == "bob"
         assert parse_username_from_string(" [L1] bob ") == "bob"
         assert parse_username_from_string("[L2] bob[L1]") == "bob[L1]"
    -    assert parse_username_from_string("[L27]bob") == "[L27]bob"
    +    assert parse_username_from_string("[L27]bob") == "bob"
         assert parse_username_from_string("[[L2]] bob[[[L1]") == "[[L2]] bob[[[L1]"
    diff --git a/grafana-plugin/src/components/Modal/Modal.tsx b/grafana-plugin/src/components/Modal/Modal.tsx
    index 146a9df80a..869a3a25b9 100644
    --- a/grafana-plugin/src/components/Modal/Modal.tsx
    +++ b/grafana-plugin/src/components/Modal/Modal.tsx
    @@ -26,6 +26,7 @@ const Modal: FC> = (props) => {
     
       return (
          = (props) => {
           {momentsToRender.map((m, i) => {
             return (
               
    -
    {m.moment.format('D MMM')}
    +
    {m.moment.format('ddd D MMM')}
    {m.moments.map((mm, j) => (
    diff --git a/grafana-plugin/src/components/UserGroups/UserGroups.tsx b/grafana-plugin/src/components/UserGroups/UserGroups.tsx index 227906834f..affdad35dc 100644 --- a/grafana-plugin/src/components/UserGroups/UserGroups.tsx +++ b/grafana-plugin/src/components/UserGroups/UserGroups.tsx @@ -104,6 +104,16 @@ const UserGroups = (props: UserGroupsProps) => { return (
    + { isMultipleGroups={isMultipleGroups} useDragHandle /> -
    ); diff --git a/grafana-plugin/src/components/UserTimezoneSelect/UserTimezoneSelect.tsx b/grafana-plugin/src/components/UserTimezoneSelect/UserTimezoneSelect.tsx index 453c578b49..37a0704d90 100644 --- a/grafana-plugin/src/components/UserTimezoneSelect/UserTimezoneSelect.tsx +++ b/grafana-plugin/src/components/UserTimezoneSelect/UserTimezoneSelect.tsx @@ -23,43 +23,64 @@ const UserTimezoneSelect: FC = (props) => { const { users, value, onChange } = props; const options = useMemo(() => { - return users.reduce((memo, user) => { - let item = memo.find((item) => item.label === user.timezone); + return users + .reduce( + (memo, user) => { + const moment = dayjs().tz(user.timezone); + const utcOffset = moment.utcOffset(); - if (!item) { - item = { - value: user.pk, - label: `${user.timezone} ${getTzOffsetString(dayjs().tz(user.timezone))}`, - imgUrl: user.avatar, - description: user.username, - }; - memo.push(item); - } else { - item.description += ', ' + user.name; - // item.imgUrl = undefined; - } + let item = memo.find((item) => item.utcOffset === utcOffset); - return memo; - }, []); - }, [users]); + if (!item) { + item = { + value: user.timezone, + utcOffset, + label: getTzOffsetString(moment), + description: user.username, + }; + memo.push(item); + } else { + item.description += item.description ? ', ' + user.username : user.username; + // item.imgUrl = undefined; + } + + return memo; + }, + [ + { + value: 'UTC', + utcOffset: 0, + label: 'GMT', + description: '', + }, + ] + ) + .sort((a, b) => { + if (b.utcOffset === 0) { + return 1; + } - const selectValue = useMemo(() => { - const user = users.find((user) => user.timezone === value); - return user?.pk; - }, [value, users]); + if (a.utcOffset > b.utcOffset) { + return 1; + } + if (a.utcOffset < b.utcOffset) { + return -1; + } + + return 0; + }); + }, [users]); const handleChange = useCallback( ({ value }) => { - const user = users.find((user) => user.pk === value); - - onChange(user?.timezone); + onChange(value); }, [users] ); return (
    -
    ); }; diff --git a/grafana-plugin/src/components/WorkingHours/WorkingHours.tsx b/grafana-plugin/src/components/WorkingHours/WorkingHours.tsx index cf25e5dd41..39b925f16b 100644 --- a/grafana-plugin/src/components/WorkingHours/WorkingHours.tsx +++ b/grafana-plugin/src/components/WorkingHours/WorkingHours.tsx @@ -25,7 +25,7 @@ interface WorkingHoursProps { const cx = cn.bind(styles); const WorkingHours: FC = (props) => { - const { timezone, workingHours, startMoment, duration, className, style } = props; + const { timezone, workingHours = default_working_hours, startMoment, duration, className, style } = props; const endMoment = startMoment.add(duration, 'seconds'); diff --git a/grafana-plugin/src/containers/RemoteSelect/RemoteSelect.tsx b/grafana-plugin/src/containers/RemoteSelect/RemoteSelect.tsx index e7ef606592..d017b149e6 100644 --- a/grafana-plugin/src/containers/RemoteSelect/RemoteSelect.tsx +++ b/grafana-plugin/src/containers/RemoteSelect/RemoteSelect.tsx @@ -28,6 +28,7 @@ interface RemoteSelectProps { openMenuOnFocus?: boolean; getOptionLabel?: (item: SelectableValue) => React.ReactNode; showError?: boolean; + maxMenuHeight?: number; } const RemoteSelect = inject('store')( @@ -48,6 +49,7 @@ const RemoteSelect = inject('store')( getOptionLabel, openMenuOnFocus = true, showError, + maxMenuHeight, } = props; const [options, setOptions] = useState(); @@ -100,6 +102,7 @@ const RemoteSelect = inject('store')( return ( // @ts-ignore void; + disabled?: boolean; + minMoment?: dayjs.Dayjs; +} + +const toDate = (moment: dayjs.Dayjs, timezone: Timezone) => { + const localMoment = dayjs().tz(timezone).utcOffset() === 0 ? moment : moment.tz(timezone); + + return new Date( + localMoment.get('year'), + localMoment.get('month'), + localMoment.get('date'), + localMoment.get('hour'), + localMoment.get('minute'), + localMoment.get('second') + ); +}; + +const DateTimePicker = (props: UserTooltipProps) => { + const { value: propValue, minMoment, timezone, onChange, disabled } = props; + + const value = useMemo(() => toDate(propValue, timezone), [propValue, timezone]); + + const minDate = useMemo(() => (minMoment ? toDate(minMoment, timezone) : undefined), [minMoment, timezone]); + + const handleDateChange = useCallback( + (newDate: Date) => { + const localMoment = dayjs().tz(timezone).utcOffset() === 0 ? dayjs().utc() : dayjs().tz(timezone); + + const newValue = localMoment + .set('year', newDate.getFullYear()) + .set('month', newDate.getMonth()) + .set('date', newDate.getDate()) + .set('hour', value.getHours()) + .set('minute', value.getMinutes()) + .set('second', value.getSeconds()); + + onChange(newValue); + }, + [value] + ); + + const handleTimeChange = useCallback( + (newMoment: DateTime) => { + const localMoment = dayjs().tz(timezone).utcOffset() === 0 ? dayjs().utc() : dayjs().tz(timezone); + const newDate = newMoment.toDate(); + const newValue = localMoment + .set('year', value.getFullYear()) + .set('month', value.getMonth()) + .set('date', value.getDate()) + .set('hour', newDate.getHours()) + .set('minute', newDate.getMinutes()) + .set('second', newDate.getSeconds()); + + onChange(newValue); + }, + [value] + ); + + return ( + + + + + ); +}; + +export default DateTimePicker; diff --git a/grafana-plugin/src/containers/RotationForm/RotationForm.module.css b/grafana-plugin/src/containers/RotationForm/RotationForm.module.css index d603e05863..b913ddb35c 100644 --- a/grafana-plugin/src/containers/RotationForm/RotationForm.module.css +++ b/grafana-plugin/src/containers/RotationForm/RotationForm.module.css @@ -4,7 +4,8 @@ .draggable { top: 0; - transition: transform 300ms ease; + + /* transition: transform 300ms ease; */ } .header { @@ -31,12 +32,8 @@ pointer-events: none; } -.date-time-picker { - display: block; -} - .inline-switch { - height: 22px; + height: 18px; } .days { @@ -58,3 +55,17 @@ .days .day__selected { background: #3d71d9; } + +.two-fields { + display: flex; + gap: 8px; + align-items: flex-start; +} + +.two-fields > div { + width: 50%; +} + +.content { + margin: 8px 0 16px 0; +} diff --git a/grafana-plugin/src/containers/RotationForm/RotationForm.tsx b/grafana-plugin/src/containers/RotationForm/RotationForm.tsx index 0a81415308..a7a2b40686 100644 --- a/grafana-plugin/src/containers/RotationForm/RotationForm.tsx +++ b/grafana-plugin/src/containers/RotationForm/RotationForm.tsx @@ -8,9 +8,10 @@ import { Field, Input, Button, - DateTimePicker, Select, InlineSwitch, + DatePickerWithInput, + TimeOfDayPicker, } from '@grafana/ui'; import cn from 'classnames/bind'; import dayjs from 'dayjs'; @@ -36,6 +37,7 @@ import { useStore } from 'state/useStore'; import { getCoords, waitForElement } from 'utils/DOM'; import { useDebouncedCallback } from 'utils/hooks'; +import DateTimePicker from './DateTimePicker'; import { RotationCreateData } from './RotationForm.types'; import styles from './RotationForm.module.css'; @@ -56,6 +58,10 @@ interface RotationFormProps { const cx = cn.bind(styles); +const repeatShiftsEveryOptions = Array.from(Array(31).keys()) + .slice(1) + .map((i) => ({ label: String(i), value: i })); + const RotationForm: FC = observer((props) => { const { onHide, @@ -78,13 +84,17 @@ const RotationForm: FC = observer((props) => { const [repeatEveryValue, setRepeatEveryValue] = useState(1); const [repeatEveryPeriod, setRepeatEveryPeriod] = useState(0); const [selectedDays, setSelectedDays] = useState([]); - const [shiftStart, setShiftStart] = useState(dateTime(shiftMoment.format('YYYY-MM-DD HH:mm:ss'))); - const [shiftEnd, setShiftEnd] = useState(dateTime(shiftMoment.add(1, 'day').format('YYYY-MM-DD HH:mm:ss'))); - const [rotationStart, setRotationStart] = useState(dateTime(shiftMoment.format('YYYY-MM-DD HH:mm:ss'))); + const [shiftStart, setShiftStart] = useState(shiftMoment); + const [shiftEnd, setShiftEnd] = useState(shiftMoment.add(1, 'day')); + const [rotationStart, setRotationStart] = useState(shiftMoment); const [endLess, setEndless] = useState(true); - const [rotationEnd, setRotationEnd] = useState( - dateTime(shiftMoment.add(1, 'month').format('YYYY-MM-DD HH:mm:ss')) - ); + const [rotationEnd, setRotationEnd] = useState(shiftMoment.add(1, 'month')); + + useEffect(() => { + if (rotationStart.isBefore(shiftStart)) { + setRotationStart(shiftStart); + } + }, [rotationStart, shiftStart]); const store = useStore(); @@ -102,7 +112,12 @@ const RotationForm: FC = observer((props) => { // elm.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' }); // setOffsetTop(Math.max(coords.top + elm.offsetHeight, 0)); - setOffsetTop(Math.max(coords.top - modal?.offsetHeight - 10, 10)); + const offsetTop = Math.min( + Math.max(coords.top - modal?.offsetHeight - 10, 10), + document.body.offsetHeight - modal?.offsetHeight - 10 + ); + + setOffsetTop(offsetTop); }); } }, [isOpen]); @@ -146,10 +161,10 @@ const RotationForm: FC = observer((props) => { const params = useMemo( () => ({ - rotation_start: getUTCString(rotationStart, currentTimezone), - until: endLess ? null : getUTCString(rotationEnd, currentTimezone), - shift_start: getUTCString(shiftStart, currentTimezone), - shift_end: getUTCString(shiftEnd, currentTimezone), + rotation_start: getUTCString(rotationStart), + until: endLess ? null : getUTCString(rotationEnd), + shift_start: getUTCString(shiftStart), + shift_end: getUTCString(shiftEnd), rolling_users: userGroups, interval: repeatEveryValue, frequency: repeatEveryPeriod, @@ -205,15 +220,15 @@ const RotationForm: FC = observer((props) => { useEffect(() => { if (shift) { - setRotationStart(getDateTime(shift.rotation_start, currentTimezone)); - setRotationEnd(getDateTime(shift.until, currentTimezone)); - setShiftStart(getDateTime(shift.shift_start, currentTimezone)); - setShiftEnd(getDateTime(shift.shift_end, currentTimezone)); + setRotationStart(getDateTime(shift.rotation_start)); + setRotationEnd(shift.until ? getDateTime(shift.until) : getDateTime(shift.shift_start).add(1, 'month')); + setShiftStart(getDateTime(shift.shift_start)); + setShiftEnd(getDateTime(shift.shift_end)); setEndless(!shift.until); setRepeatEveryValue(shift.interval); setRepeatEveryPeriod(shift.frequency); - setSelectedDays(shift.by_day); + setSelectedDays(shift.by_day || []); setUserGroups(shift.rolling_users); } @@ -230,6 +245,8 @@ const RotationForm: FC = observer((props) => { setRepeatEveryValue(option.value); }, []); + const isFormValid = useMemo(() => userGroups.some((group) => group.length), [userGroups]); + const moment = dayjs(); return ( @@ -252,8 +269,8 @@ const RotationForm: FC = observer((props) => { - - + {/* + */} {shiftId !== 'new' && ( @@ -262,116 +279,115 @@ const RotationForm: FC = observer((props) => { - group.length)} - /> {/*
    */} - - - - { - setEndless(false); - }} + } + > + - ) : ( - - )} - - - + + + + Rotation end + + + + } + > + {endLess ? ( +
    + Endless +
    + ) : ( + + )} +
    +
    + + +