commit
49789098d8
43 changed files with 853 additions and 893 deletions
2
.github/workflows/helm_release.yml
vendored
2
.github/workflows/helm_release.yml
vendored
|
|
@ -13,6 +13,6 @@ jobs:
|
|||
cr_configfile: helm/cr.yaml
|
||||
ct_configfile: helm/ct.yaml
|
||||
secrets:
|
||||
helm_repo_token: ${{ secrets.GH_BOT_ACCESS_TOKEN }}
|
||||
helm_repo_token: ${{ secrets.GH_HELM_RELEASE }}
|
||||
# See https://github.com/grafana/helm-charts/blob/main/INTERNAL.md about this key
|
||||
gpg_key_base64: ${{ secrets.HELM_SIGN_KEY_BASE64 }}
|
||||
|
|
|
|||
2
.github/workflows/issue_commands.yml
vendored
2
.github/workflows/issue_commands.yml
vendored
|
|
@ -19,5 +19,5 @@ jobs:
|
|||
- name: Run Commands
|
||||
uses: ./actions/commands
|
||||
with:
|
||||
token: ${{secrets.GH_BOT_ACCESS_TOKEN}}
|
||||
token: ${{secrets.GH_ISSUE_COMMANDS}}
|
||||
configPath: issue_and_pr_commands
|
||||
|
|
@ -28,7 +28,9 @@ jobs:
|
|||
uses: "actions/checkout@v3"
|
||||
|
||||
- name: "Clone website-sync Action"
|
||||
run: "git clone --single-branch --no-tags --depth 1 -b master https://grafanabot:${{ secrets.GH_BOT_ACCESS_TOKEN }}@github.com/grafana/website-sync ./.github/actions/website-sync"
|
||||
# WEBSITE_SYNC_ONCALL is a fine-grained GitHub Personal Access Token that expires.
|
||||
# It must be updated in the grafanabot GitHub account.
|
||||
run: "git clone --single-branch --no-tags --depth 1 -b master https://grafanabot:${{ secrets.WEBSITE_SYNC_ONCALL }}@github.com/grafana/website-sync ./.github/actions/website-sync"
|
||||
|
||||
- name: "Publish to website repository (next)"
|
||||
uses: "./.github/actions/website-sync"
|
||||
|
|
@ -37,6 +39,8 @@ jobs:
|
|||
repository: "grafana/website"
|
||||
branch: "master"
|
||||
host: "github.com"
|
||||
github_pat: "${{ secrets.GH_BOT_ACCESS_TOKEN }}"
|
||||
# PUBLISH_TO_WEBSITE_ONCALL is a fine-grained GitHub Personal Access Token that expires.
|
||||
# It must be updated in the grafanabot GitHub account.
|
||||
github_pat: "grafanabot:${{ secrets.PUBLISH_TO_WEBSITE_ONCALL }}"
|
||||
source_folder: "docs/sources"
|
||||
target_folder: "content/docs/oncall/next"
|
||||
|
|
|
|||
|
|
@ -58,7 +58,9 @@ jobs:
|
|||
|
||||
- name: "Clone website-sync Action"
|
||||
if: "steps.has-matching-release-tag.outputs.bool == 'true'"
|
||||
run: "git clone --single-branch --no-tags --depth 1 -b master https://grafanabot:${{ secrets.GH_BOT_ACCESS_TOKEN }}@github.com/grafana/website-sync ./.github/actions/website-sync"
|
||||
# WEBSITE_SYNC_ONCALL is a fine-grained GitHub Personal Access Token that expires.
|
||||
# It must be updated in the grafanabot GitHub account.
|
||||
run: "git clone --single-branch --no-tags --depth 1 -b master https://grafanabot:${{ secrets.WEBSITE_SYNC_ONCALL }}@github.com/grafana/website-sync ./.github/actions/website-sync"
|
||||
|
||||
- name: "Publish to website repository (release)"
|
||||
if: "steps.has-matching-release-tag.outputs.bool == 'true'"
|
||||
|
|
@ -68,7 +70,9 @@ jobs:
|
|||
repository: "grafana/website"
|
||||
branch: "master"
|
||||
host: "github.com"
|
||||
github_pat: "${{ secrets.GH_BOT_ACCESS_TOKEN }}"
|
||||
# PUBLISH_TO_WEBSITE_ONCALL is a fine-grained GitHub Personal Access Token that expires.
|
||||
# It must be updated in the grafanabot GitHub account.
|
||||
github_pat: "grafanabot:${{ secrets.PUBLISH_TO_WEBSITE_ONCALL }}"
|
||||
source_folder: "docs/sources"
|
||||
# Append ".x" to target to produce a v<major>.<minor>.x directory.
|
||||
target_folder: "content/docs/oncall/${{ steps.target.outputs.target }}.x"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ repos:
|
|||
- id: isort
|
||||
name: isort - pd-migrator
|
||||
files: ^tools/pagerduty-migrator
|
||||
args: [--settings-file=tools/pagerduty-migrator/.isort.cfg, --filter-files]
|
||||
args:
|
||||
[--settings-file=tools/pagerduty-migrator/.isort.cfg, --filter-files]
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.3.0
|
||||
|
|
@ -33,7 +34,12 @@ repos:
|
|||
files: ^tools/pagerduty-migrator
|
||||
# Make sure config is compatible with black
|
||||
# https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#line-length
|
||||
args: [--max-line-length=88, "--select=C,E,F,W,B,B950", "--extend-ignore=E203,E501"]
|
||||
args:
|
||||
[
|
||||
--max-line-length=88,
|
||||
"--select=C,E,F,W,B,B950",
|
||||
"--extend-ignore=E203,E501",
|
||||
]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||
rev: v8.25.0
|
||||
|
|
@ -56,12 +62,12 @@ repos:
|
|||
types_or: [css, javascript, jsx, ts, tsx]
|
||||
files: ^grafana-plugin/src
|
||||
additional_dependencies:
|
||||
- prettier@^2.7.1
|
||||
- prettier@2.8.2
|
||||
- id: prettier
|
||||
name: prettier - json
|
||||
types_or: [json]
|
||||
additional_dependencies:
|
||||
- prettier@^2.7.1
|
||||
- prettier@2.8.2
|
||||
|
||||
- repo: https://github.com/thibaudcolas/pre-commit-stylelint
|
||||
rev: v13.13.1
|
||||
|
|
|
|||
12
CHANGELOG.md
12
CHANGELOG.md
|
|
@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## v1.1.15 (2023-01-10)
|
||||
|
||||
### Changed
|
||||
|
||||
- Simplify and speed up slack rendering (#1105)
|
||||
- Faro - Point to 3 separate apps instead of just 1 for all environments (#1110)
|
||||
- Schedules - (#1114 #1109)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bugfix for topnavbar to place alerts inside PageNav (#1040)
|
||||
|
||||
## v1.1.14 (2023-01-05)
|
||||
|
||||
### Changed
|
||||
|
|
|
|||
|
|
@ -74,41 +74,14 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
|
|||
return AlertSlackRenderer
|
||||
|
||||
def render_alert_group_blocks(self):
|
||||
non_resolve_alerts_queryset = self.alert_group.alerts.filter(is_resolve_signal=False)
|
||||
if not self.alert_group.channel.organization.slack_team_identity.installed_via_granular_permissions:
|
||||
blocks = [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": ":warning: *Action required - reinstall app*\n"
|
||||
"Slack is deprecating current permission model. We will support it till DATE\n" # TODO: deprecation date
|
||||
"Don't worry - we migrate OnCall to new one, but it required to reinstall app."
|
||||
'Press "Upgrade" button to see more detailed instruction and upgrade.',
|
||||
},
|
||||
},
|
||||
{"type": "divider"},
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Upgrade",
|
||||
},
|
||||
"value": "click_me_123",
|
||||
"url": self.alert_group.channel.organization.web_slack_page_link,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
else:
|
||||
blocks = []
|
||||
if non_resolve_alerts_queryset.count() <= 1:
|
||||
blocks.extend(self.alert_renderer.render_alert_blocks())
|
||||
else:
|
||||
blocks.extend(self._get_alert_group_base_blocks_if_grouped())
|
||||
blocks = self.alert_renderer.render_alert_blocks()
|
||||
alerts_count = self.alert_group.alerts.count()
|
||||
if alerts_count > 1:
|
||||
text = (
|
||||
f":package: Showing the last alert only out of {alerts_count} total. "
|
||||
f"Visit <{self.alert_group.web_link}|the plugin page> to see them all."
|
||||
)
|
||||
blocks.append({"type": "context", "elements": [{"type": "mrkdwn", "text": text}]})
|
||||
return blocks
|
||||
|
||||
def render_alert_group_attachments(self):
|
||||
|
|
@ -189,23 +162,6 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
|
|||
attachment["color"] = color
|
||||
return attachments
|
||||
|
||||
def _get_text_alert_grouped(self):
|
||||
alert_count = self.alert_group.alerts.count()
|
||||
link = self.alert_group.web_link
|
||||
|
||||
text = (
|
||||
f":package: Showing the last alert only out of {alert_count} total. "
|
||||
f"Visit <{link}|the plugin page> to see them all."
|
||||
)
|
||||
|
||||
return text
|
||||
|
||||
def _get_alert_group_base_blocks_if_grouped(self):
|
||||
text = self._get_text_alert_grouped()
|
||||
blocks = self.alert_renderer.render_alert_blocks()
|
||||
blocks.append({"type": "context", "elements": [{"type": "mrkdwn", "text": text}]})
|
||||
return blocks
|
||||
|
||||
def _get_buttons_attachments(self):
|
||||
attachment = {"blocks": self._get_buttons_blocks()}
|
||||
return attachment
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from common.api_helpers.mixins import EagerLoadingMixin
|
|||
|
||||
|
||||
class PolymorphicScheduleSerializer(EagerLoadingMixin, PolymorphicSerializer):
|
||||
SELECT_RELATED = ["organization"]
|
||||
SELECT_RELATED = ["organization", "user_group"]
|
||||
|
||||
resource_type_field_name = "type"
|
||||
|
||||
|
|
|
|||
|
|
@ -135,9 +135,7 @@ class ScheduleView(
|
|||
organization = self.request.auth.organization
|
||||
queryset = OnCallSchedule.objects.filter(organization=organization, team=self.request.user.current_team).defer(
|
||||
# avoid requesting large text fields which are not used when listing schedules
|
||||
"cached_ical_file_primary",
|
||||
"prev_ical_file_primary",
|
||||
"cached_ical_file_overrides",
|
||||
"prev_ical_file_overrides",
|
||||
)
|
||||
if not is_short_request:
|
||||
|
|
|
|||
|
|
@ -111,19 +111,6 @@ def send_link_to_channel_message_or_fallback_to_full_alert_group(
|
|||
)
|
||||
|
||||
|
||||
@shared_dedicated_queue_retry_task(bind=True, autoretry_for=(Exception,), retry_backoff=True, max_retries=None)
|
||||
def send_link_to_channel_message_or_fallback_to_full_incident(
|
||||
self, alert_group_pk, notification_policy_pk, user_connector_pk
|
||||
):
|
||||
"""
|
||||
Deprecated task, use send_link_to_channel_message_or_fallback_to_full_alert_group above instead.
|
||||
TODO: remove this task after releasing the new version of the task
|
||||
"""
|
||||
send_link_to_channel_message_or_fallback_to_full_alert_group(
|
||||
self, alert_group_pk, notification_policy_pk, user_connector_pk
|
||||
)
|
||||
|
||||
|
||||
@shared_dedicated_queue_retry_task(
|
||||
bind=True, autoretry_for=(Exception,), retry_backoff=True, max_retries=1 if settings.DEBUG else None
|
||||
)
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ CELERY_TASK_ROUTES = {
|
|||
"apps.telegram.tasks.edit_message": {"queue": "telegram"},
|
||||
"apps.telegram.tasks.on_create_alert_telegram_representative_async": {"queue": "telegram"},
|
||||
"apps.telegram.tasks.register_telegram_webhook": {"queue": "telegram"},
|
||||
"apps.telegram.tasks.send_link_to_channel_message_or_fallback_to_full_incident": {"queue": "telegram"},
|
||||
"apps.telegram.tasks.send_link_to_channel_message_or_fallback_to_full_alert_group": {"queue": "telegram"},
|
||||
"apps.telegram.tasks.send_log_and_actions_message": {"queue": "telegram"},
|
||||
# WEBHOOK
|
||||
"apps.alerts.tasks.custom_button_result.custom_button_result": {"queue": "webhook"},
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@
|
|||
"eslint-plugin-import": "^2.25.4",
|
||||
"mobx": "5.13.0",
|
||||
"mobx-react": "6.1.1",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier": "^2.8.2",
|
||||
"rc-table": "^7.17.1",
|
||||
"react-copy-to-clipboard": "^5.0.2",
|
||||
"react-draggable": "^4.4.5",
|
||||
|
|
|
|||
|
|
@ -6,25 +6,34 @@ import Header from 'navbar/Header/Header';
|
|||
import { pages } from 'pages';
|
||||
import { isTopNavbar } from 'plugin/GrafanaPluginRootPage.helpers';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { DEFAULT_PAGE } from 'utils/consts';
|
||||
import { useQueryParams } from 'utils/hooks';
|
||||
|
||||
export const PluginPage = (isTopNavbar() ? RealPlugin : PluginPageFallback) as React.ComponentType<PluginPageProps>;
|
||||
export const PluginPage = (
|
||||
isTopNavbar() ? RealPlugin : PluginPageFallback
|
||||
) as React.ComponentType<ExtendedPluginPageProps>;
|
||||
|
||||
function RealPlugin(props: PluginPageProps): React.ReactNode {
|
||||
interface ExtendedPluginPageProps extends PluginPageProps {
|
||||
renderAlertsFn?: () => React.ReactNode;
|
||||
}
|
||||
|
||||
function RealPlugin(props: ExtendedPluginPageProps): React.ReactNode {
|
||||
const store = useStore();
|
||||
|
||||
const queryParams = useQueryParams();
|
||||
const page = queryParams.get('page');
|
||||
const page = queryParams.get('page') || DEFAULT_PAGE;
|
||||
|
||||
return (
|
||||
<RealPluginPage {...props}>
|
||||
{/* Render alerts at the top */}
|
||||
{props.renderAlertsFn && props.renderAlertsFn()}
|
||||
<Header page={page} backendLicense={store.backendLicense} />
|
||||
<h3 className="page-title">{pages[page].text}</h3>
|
||||
{pages[page].text && <h3 className="page-title">{pages[page].text}</h3>}
|
||||
{props.children}
|
||||
</RealPluginPage>
|
||||
);
|
||||
}
|
||||
|
||||
function PluginPageFallback(props: PluginPageProps): React.ReactNode {
|
||||
function PluginPageFallback(props: ExtendedPluginPageProps): React.ReactNode {
|
||||
return props.children;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,32 @@
|
|||
.root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.alerts_horizontal {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
.alerts-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 24px;
|
||||
gap: 10px;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.alert {
|
||||
margin: 24px 0;
|
||||
.navbar-legacy .alerts-container {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.navbar-legacy {
|
||||
padding-top: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,24 @@
|
|||
import plugin from '../../../package.json'; // eslint-disable-line
|
||||
import React, { FC, useEffect, useState, useCallback } from 'react';
|
||||
|
||||
import { Alert } from '@grafana/ui';
|
||||
import { PluginPage } from 'PluginPage';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
import { AppRootProps } from 'types';
|
||||
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import { getIfChatOpsConnected } from 'containers/DefaultPageLayout/helper';
|
||||
import { pages } from 'pages';
|
||||
import { isTopNavbar } from 'plugin/GrafanaPluginRootPage.helpers';
|
||||
import { AppFeature } from 'state/features';
|
||||
import { useStore } from 'state/useStore';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
import { isUserActionAllowed, UserActions } from 'utils/authorization';
|
||||
import { GRAFANA_LICENSE_OSS } from 'utils/consts';
|
||||
import { useForceUpdate } from 'utils/hooks';
|
||||
import { DEFAULT_PAGE, GRAFANA_LICENSE_OSS } from 'utils/consts';
|
||||
import { useForceUpdate, useQueryParams } from 'utils/hooks';
|
||||
|
||||
import plugin from '../../../package.json'; // eslint-disable-line
|
||||
|
||||
import { getItem, setItem } from 'utils/localStorage';
|
||||
import sanitize from 'utils/sanitize';
|
||||
|
||||
|
|
@ -33,10 +38,12 @@ enum AlertID {
|
|||
|
||||
const DefaultPageLayout: FC<DefaultPageLayoutProps> = observer((props) => {
|
||||
const { children, query } = props;
|
||||
const queryParams = useQueryParams();
|
||||
|
||||
const [showSlackInstallAlert, setShowSlackInstallAlert] = useState<SlackError | undefined>();
|
||||
|
||||
const forceUpdate = useForceUpdate();
|
||||
const page = queryParams.get('page') || DEFAULT_PAGE;
|
||||
|
||||
const handleCloseInstallSlackAlert = useCallback(() => {
|
||||
setShowSlackInstallAlert(undefined);
|
||||
|
|
@ -68,15 +75,41 @@ const DefaultPageLayout: FC<DefaultPageLayoutProps> = observer((props) => {
|
|||
const isChatOpsConnected = getIfChatOpsConnected(currentUser);
|
||||
const isPhoneVerified = currentUser?.cloud_connection_status === 3 || currentUser?.verified_phone_number;
|
||||
|
||||
return (
|
||||
<div className={cx('root')}>
|
||||
<div className={styles.alerts_horizontal}>
|
||||
if (isTopNavbar()) {
|
||||
return renderTopNavbar();
|
||||
}
|
||||
|
||||
return renderLegacyNavbar();
|
||||
|
||||
function renderTopNavbar(): JSX.Element {
|
||||
return (
|
||||
<PluginPage pageNav={pages[page].getPageNav()} renderAlertsFn={renderAlertsFn}>
|
||||
<div className={cx('root')}>{children}</div>
|
||||
</PluginPage>
|
||||
);
|
||||
}
|
||||
|
||||
function renderLegacyNavbar(): JSX.Element {
|
||||
return (
|
||||
<PluginPage>
|
||||
<div className="page-container u-height-100">
|
||||
<div className={cx('root', 'navbar-legacy')}>
|
||||
{renderAlertsFn()}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</PluginPage>
|
||||
);
|
||||
}
|
||||
|
||||
function renderAlertsFn(): JSX.Element {
|
||||
return (
|
||||
<div className={cx('alerts-container')}>
|
||||
{showSlackInstallAlert && (
|
||||
<Alert
|
||||
className={styles.alert}
|
||||
className={cx('alert')}
|
||||
onRemove={handleCloseInstallSlackAlert}
|
||||
severity="warning"
|
||||
// @ts-ignore
|
||||
title="Slack integration warning"
|
||||
>
|
||||
{getSlackMessage(
|
||||
|
|
@ -88,7 +121,7 @@ const DefaultPageLayout: FC<DefaultPageLayoutProps> = observer((props) => {
|
|||
)}
|
||||
{currentTeam?.banner.title != null && !getItem(currentTeam?.banner.title) && (
|
||||
<Alert
|
||||
className={styles.alert}
|
||||
className={cx('alert')}
|
||||
severity="success"
|
||||
title={currentTeam.banner.title}
|
||||
onRemove={getRemoveAlertHandler(currentTeam?.banner.title)}
|
||||
|
|
@ -106,7 +139,7 @@ const DefaultPageLayout: FC<DefaultPageLayoutProps> = observer((props) => {
|
|||
store.backendVersion !== plugin?.version &&
|
||||
!getItem(`version_mismatch_${store.backendVersion}_${plugin?.version}`) && (
|
||||
<Alert
|
||||
className={styles.alert}
|
||||
className={cx('alert')}
|
||||
severity="warning"
|
||||
title={'Version mismatch!'}
|
||||
onRemove={getRemoveAlertHandler(`version_mismatch_${store.backendVersion}_${plugin?.version}`)}
|
||||
|
|
@ -137,7 +170,7 @@ const DefaultPageLayout: FC<DefaultPageLayoutProps> = observer((props) => {
|
|||
) && (
|
||||
<Alert
|
||||
onRemove={getRemoveAlertHandler(AlertID.CONNECTIVITY_WARNING)}
|
||||
className={styles.alert}
|
||||
className={cx('alert')}
|
||||
severity="warning"
|
||||
// @ts-ignore
|
||||
title="Connectivity Warning"
|
||||
|
|
@ -160,9 +193,8 @@ const DefaultPageLayout: FC<DefaultPageLayoutProps> = observer((props) => {
|
|||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default DefaultPageLayout;
|
||||
|
|
|
|||
|
|
@ -1,24 +1,17 @@
|
|||
.teamSelect {
|
||||
width: 200px;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
padding: 16px 0;
|
||||
margin-right: 24px;
|
||||
|
||||
&--topRight {
|
||||
right: 14px;
|
||||
top: 12px;
|
||||
}
|
||||
&--topRightIncident {
|
||||
right: 32px;
|
||||
top: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.teamSelectLabel {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.teamSelectText,
|
||||
.teamSelectLink {
|
||||
line-height: 1.25;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.teamSelectLink {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,13 +49,15 @@ const GrafanaTeamSelect = observer((props: GrafanaTeamSelectProps) => {
|
|||
};
|
||||
|
||||
const content = (
|
||||
<div className={cx('teamSelect', { 'teamSelect--topRight': isTopNavbar() })}>
|
||||
<div className={cx('teamSelect')}>
|
||||
<div className={cx('teamSelectLabel')}>
|
||||
<Label>
|
||||
Select Team{' '}
|
||||
<Tooltip content="The objects on this page are filtered by team and you can only view the objects that belong to your team. Note that filtering within Grafana OnCall is meant for usability, not access management.">
|
||||
<Icon name="info-circle" size="md" className={cx('teamSelectInfo')}></Icon>
|
||||
</Tooltip>
|
||||
<span className={cx('teamSelectText')}>
|
||||
Select Team{''}
|
||||
<Tooltip content="The objects on this page are filtered by team and you can only view the objects that belong to your team. Note that filtering within Grafana OnCall is meant for usability, not access management.">
|
||||
<Icon name="info-circle" size="md" className={cx('teamSelectInfo')}></Icon>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</Label>
|
||||
<WithPermissionControl userAction={UserActions.TeamsWrite}>
|
||||
<PluginLink path="/org/teams" className={cx('teamSelectLink')}>
|
||||
|
|
|
|||
|
|
@ -4,24 +4,12 @@
|
|||
max-width: unset !important;
|
||||
}
|
||||
|
||||
.oncall-header {
|
||||
padding-top: 0;
|
||||
padding-bottom: 36px;
|
||||
}
|
||||
|
||||
.scrollbar-view h1:first-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.page-container.page-body {
|
||||
flex-grow: 1 !important;
|
||||
[class$='-page-header'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
max-width: unset !important;
|
||||
flex-grow: unset !important;
|
||||
flex-basis: unset !important;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.page-scrollbar-content > div:first-child {
|
||||
|
|
@ -34,34 +22,6 @@
|
|||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* This is for Grafana 8, remove later */
|
||||
@media (max-width: 1540px) {
|
||||
.page-header__tabs > ul > li > a > div {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1540px) {
|
||||
.page-header__tabs > div > div > a > div {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1300px) {
|
||||
.sidemenu {
|
||||
position: fixed !important;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.main-view {
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
.page-header__tabs li a {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header__info-block {
|
||||
flex-grow: 1; /* Stretch the navigation subtitle panel */
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const tzs = [
|
||||
const tzs: string[] = [
|
||||
'Africa/Abidjan',
|
||||
'Africa/Accra',
|
||||
'Africa/Addis_Ababa',
|
||||
|
|
@ -591,6 +591,6 @@ const tzs = [
|
|||
'W-SU',
|
||||
'WET',
|
||||
'Zulu',
|
||||
] as const;
|
||||
];
|
||||
|
||||
export type Timezone = typeof tzs[number];
|
||||
export type Timezone = (typeof tzs)[number];
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@
|
|||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.header-topnavbar {
|
||||
padding-top: 0;
|
||||
padding-bottom: 36px;
|
||||
}
|
||||
|
||||
.navbar-heading {
|
||||
padding: 4px;
|
||||
margin: 0 0 0 8px;
|
||||
|
|
@ -16,3 +21,8 @@
|
|||
align-items: center;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.navbar-left {
|
||||
display: flex;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Card } from '@grafana/ui';
|
||||
import classnames from 'classnames';
|
||||
import cn from 'classnames/bind';
|
||||
|
||||
import gitHubStarSVG from 'assets/img/github_star.svg';
|
||||
|
|
@ -16,15 +15,16 @@ const cx = cn.bind(styles);
|
|||
|
||||
export default function Header({ page, backendLicense }: { page: string; backendLicense: string }) {
|
||||
return (
|
||||
<div className="page-container">
|
||||
<div className="page-header">
|
||||
<div className={classnames('page-header__inner', { 'oncall-header': isTopNavbar() })}>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('page-header__inner', { 'header-topnavbar': isTopNavbar() })}>
|
||||
<div className={cx('navbar-left')}>
|
||||
<span className="page-header__logo">
|
||||
<img className="page-header__img" src={logo} alt="Grafana OnCall" />
|
||||
</span>
|
||||
|
||||
<div className="page-header__info-block">{renderHeading()}</div>
|
||||
|
||||
</div>
|
||||
<div className={cx('navbar-right')}>
|
||||
<GrafanaTeamSelect currentPage={page} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
.root {
|
||||
min-width: 1500px;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,16 +19,18 @@ export default function LegacyNavTabsBar({ currentPage }: { currentPage: string
|
|||
.filter((page) => (page.hideFromTabsFn ? !page.hideFromTabsFn(store) : !page.hideFromTabs));
|
||||
|
||||
return (
|
||||
<TabsBar className={cx('root')}>
|
||||
{navigationPages.map((page, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
icon={page.icon as IconName}
|
||||
label={page.text}
|
||||
href={page.path}
|
||||
active={currentPage === page.id}
|
||||
/>
|
||||
))}
|
||||
</TabsBar>
|
||||
<div className={cx('root')}>
|
||||
<TabsBar>
|
||||
{navigationPages.map((page, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
icon={page.icon as IconName}
|
||||
label={page.text}
|
||||
href={page.path}
|
||||
active={currentPage === page.id}
|
||||
/>
|
||||
))}
|
||||
</TabsBar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Button, HorizontalGroup, Icon, IconButton, LoadingPlaceholder, Tooltip, VerticalGroup } from '@grafana/ui';
|
||||
import { PluginPage } from 'PluginPage';
|
||||
import cn from 'classnames/bind';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
|
|
@ -25,7 +24,6 @@ import EscalationChainForm from 'containers/EscalationChainForm/EscalationChainF
|
|||
import EscalationChainSteps from 'containers/EscalationChainSteps/EscalationChainSteps';
|
||||
import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl';
|
||||
import { EscalationChain } from 'models/escalation_chain/escalation_chain.types';
|
||||
import { pages } from 'pages';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
|
|
@ -135,90 +133,88 @@ class EscalationChainsPage extends React.Component<EscalationChainsPageProps, Es
|
|||
const searchResult = escalationChainStore.getSearchResult(escalationChainsFilters.searchTerm);
|
||||
|
||||
return (
|
||||
<PluginPage pageNav={pages['escalations'].getPageNav()}>
|
||||
<PageErrorHandlingWrapper
|
||||
errorData={errorData}
|
||||
objectName="escalation"
|
||||
pageName="escalations"
|
||||
itemNotFoundMessage={`Escalation chain with id=${query?.id} is not found. Please select escalation chain from the list.`}
|
||||
>
|
||||
{() => (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('filters')}>
|
||||
<EscalationsFilters value={escalationChainsFilters} onChange={this.handleEscalationsFiltersChange} />
|
||||
<PageErrorHandlingWrapper
|
||||
errorData={errorData}
|
||||
objectName="escalation"
|
||||
pageName="escalations"
|
||||
itemNotFoundMessage={`Escalation chain with id=${query?.id} is not found. Please select escalation chain from the list.`}
|
||||
>
|
||||
{() => (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('filters')}>
|
||||
<EscalationsFilters value={escalationChainsFilters} onChange={this.handleEscalationsFiltersChange} />
|
||||
</div>
|
||||
{!searchResult || searchResult.length ? (
|
||||
<div className={cx('escalations')}>
|
||||
<div className={cx('left-column')}>
|
||||
<WithPermissionControl userAction={UserActions.IntegrationsWrite}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.setState({ showCreateEscalationChainModal: true });
|
||||
}}
|
||||
icon="plus"
|
||||
className={cx('new-escalation-chain')}
|
||||
>
|
||||
New Escalation Chain
|
||||
</Button>
|
||||
</WithPermissionControl>
|
||||
<div className={cx('escalations-list')}>
|
||||
{searchResult ? (
|
||||
<GList
|
||||
autoScroll
|
||||
selectedId={selectedEscalationChain}
|
||||
items={searchResult}
|
||||
itemKey="id"
|
||||
onSelect={this.setSelectedEscalationChain}
|
||||
>
|
||||
{(item) => <EscalationChainCard id={item.id} />}
|
||||
</GList>
|
||||
) : (
|
||||
<LoadingPlaceholder className={cx('loading')} text="Loading..." />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cx('escalation')}>{this.renderEscalation()}</div>
|
||||
</div>
|
||||
{!searchResult || searchResult.length ? (
|
||||
<div className={cx('escalations')}>
|
||||
<div className={cx('left-column')}>
|
||||
<WithPermissionControl userAction={UserActions.IntegrationsWrite}>
|
||||
) : (
|
||||
<Tutorial
|
||||
step={TutorialStep.Escalations}
|
||||
title={
|
||||
<VerticalGroup align="center" spacing="lg">
|
||||
<Text type="secondary">No escalations found, check your filtering and current team.</Text>
|
||||
<WithPermissionControl userAction={UserActions.EscalationChainsWrite}>
|
||||
<Button
|
||||
icon="plus"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
this.setState({ showCreateEscalationChainModal: true });
|
||||
}}
|
||||
icon="plus"
|
||||
className={cx('new-escalation-chain')}
|
||||
>
|
||||
New Escalation Chain
|
||||
</Button>
|
||||
</WithPermissionControl>
|
||||
<div className={cx('escalations-list')}>
|
||||
{searchResult ? (
|
||||
<GList
|
||||
autoScroll
|
||||
selectedId={selectedEscalationChain}
|
||||
items={searchResult}
|
||||
itemKey="id"
|
||||
onSelect={this.setSelectedEscalationChain}
|
||||
>
|
||||
{(item) => <EscalationChainCard id={item.id} />}
|
||||
</GList>
|
||||
) : (
|
||||
<LoadingPlaceholder className={cx('loading')} text="Loading..." />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cx('escalation')}>{this.renderEscalation()}</div>
|
||||
</div>
|
||||
) : (
|
||||
<Tutorial
|
||||
step={TutorialStep.Escalations}
|
||||
title={
|
||||
<VerticalGroup align="center" spacing="lg">
|
||||
<Text type="secondary">No escalations found, check your filtering and current team.</Text>
|
||||
<WithPermissionControl userAction={UserActions.EscalationChainsWrite}>
|
||||
<Button
|
||||
icon="plus"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
this.setState({ showCreateEscalationChainModal: true });
|
||||
}}
|
||||
>
|
||||
New Escalation Chain
|
||||
</Button>
|
||||
</WithPermissionControl>
|
||||
</VerticalGroup>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{showCreateEscalationChainModal && (
|
||||
<EscalationChainForm
|
||||
escalationChainId={escalationChainIdToCopy}
|
||||
onHide={() => {
|
||||
this.setState({
|
||||
showCreateEscalationChainModal: false,
|
||||
escalationChainIdToCopy: undefined,
|
||||
});
|
||||
}}
|
||||
onUpdate={this.handleEscalationChainCreate}
|
||||
</VerticalGroup>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</PageErrorHandlingWrapper>
|
||||
</PluginPage>
|
||||
</div>
|
||||
{showCreateEscalationChainModal && (
|
||||
<EscalationChainForm
|
||||
escalationChainId={escalationChainIdToCopy}
|
||||
onHide={() => {
|
||||
this.setState({
|
||||
showCreateEscalationChainModal: false,
|
||||
escalationChainIdToCopy: undefined,
|
||||
});
|
||||
}}
|
||||
onUpdate={this.handleEscalationChainCreate}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</PageErrorHandlingWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@
|
|||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.block {
|
||||
padding: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.payload-subtitle {
|
||||
margin-bottom: var(--title-marginBottom);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import {
|
|||
Modal,
|
||||
Tooltip,
|
||||
} from '@grafana/ui';
|
||||
import { PluginPage } from 'PluginPage';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
import moment from 'moment-timezone';
|
||||
|
|
@ -46,7 +45,6 @@ import {
|
|||
GroupedAlert,
|
||||
} from 'models/alertgroup/alertgroup.types';
|
||||
import { ResolutionNoteSourceTypesToDisplayName } from 'models/resolution_note/resolution_note.types';
|
||||
import { pages } from 'pages';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
|
|
@ -127,71 +125,69 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
}
|
||||
|
||||
return (
|
||||
<PluginPage pageNav={pages['incident'].getPageNav()}>
|
||||
<PageErrorHandlingWrapper errorData={errorData} objectName="alert group" pageName="incidents">
|
||||
{() => (
|
||||
<div className={cx('root')}>
|
||||
{errorData.isNotFoundError ? (
|
||||
<div className={cx('not-found')}>
|
||||
<VerticalGroup spacing="lg" align="center">
|
||||
<Text.Title level={1}>404</Text.Title>
|
||||
<Text.Title level={4}>Incident not found</Text.Title>
|
||||
<PluginLink query={{ page: 'incidents', cursor, start, perpage }}>
|
||||
<Button variant="secondary" icon="arrow-left" size="md">
|
||||
Go to incidents page
|
||||
</Button>
|
||||
</PluginLink>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{this.renderHeader()}
|
||||
<div className={cx('content')}>
|
||||
<div className={cx('column')}>
|
||||
<Incident incident={incident} datetimeReference={this.getIncidentDatetimeReference(incident)} />
|
||||
<GroupedIncidentsList
|
||||
id={incident.pk}
|
||||
getIncidentDatetimeReference={this.getIncidentDatetimeReference}
|
||||
/>
|
||||
<AttachedIncidentsList id={incident.pk} getUnattachClickHandler={this.getUnattachClickHandler} />
|
||||
</div>
|
||||
<div className={cx('column')}>{this.renderTimeline()}</div>
|
||||
<PageErrorHandlingWrapper errorData={errorData} objectName="alert group" pageName="incidents">
|
||||
{() => (
|
||||
<div className={cx('root')}>
|
||||
{errorData.isNotFoundError ? (
|
||||
<div className={cx('not-found')}>
|
||||
<VerticalGroup spacing="lg" align="center">
|
||||
<Text.Title level={1}>404</Text.Title>
|
||||
<Text.Title level={4}>Incident not found</Text.Title>
|
||||
<PluginLink query={{ page: 'incidents', cursor, start, perpage }}>
|
||||
<Button variant="secondary" icon="arrow-left" size="md">
|
||||
Go to incidents page
|
||||
</Button>
|
||||
</PluginLink>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{this.renderHeader()}
|
||||
<div className={cx('content')}>
|
||||
<div className={cx('column')}>
|
||||
<Incident incident={incident} datetimeReference={this.getIncidentDatetimeReference(incident)} />
|
||||
<GroupedIncidentsList
|
||||
id={incident.pk}
|
||||
getIncidentDatetimeReference={this.getIncidentDatetimeReference}
|
||||
/>
|
||||
<AttachedIncidentsList id={incident.pk} getUnattachClickHandler={this.getUnattachClickHandler} />
|
||||
</div>
|
||||
{showIntegrationSettings && (
|
||||
<IntegrationSettings
|
||||
alertGroupId={incident.pk}
|
||||
onUpdate={() => {
|
||||
alertReceiveChannelStore.updateItem(incident.alert_receive_channel.id);
|
||||
}}
|
||||
onUpdateTemplates={() => {
|
||||
store.alertGroupStore.getAlert(id);
|
||||
}}
|
||||
startTab={IntegrationSettingsTab.Templates}
|
||||
id={incident.alert_receive_channel.id}
|
||||
onHide={() =>
|
||||
this.setState({
|
||||
showIntegrationSettings: undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{showAttachIncidentForm && (
|
||||
<AttachIncidentForm
|
||||
id={id}
|
||||
onHide={() => {
|
||||
this.setState({
|
||||
showAttachIncidentForm: false,
|
||||
});
|
||||
}}
|
||||
onUpdate={this.update}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</PageErrorHandlingWrapper>
|
||||
</PluginPage>
|
||||
<div className={cx('column')}>{this.renderTimeline()}</div>
|
||||
</div>
|
||||
{showIntegrationSettings && (
|
||||
<IntegrationSettings
|
||||
alertGroupId={incident.pk}
|
||||
onUpdate={() => {
|
||||
alertReceiveChannelStore.updateItem(incident.alert_receive_channel.id);
|
||||
}}
|
||||
onUpdateTemplates={() => {
|
||||
store.alertGroupStore.getAlert(id);
|
||||
}}
|
||||
startTab={IntegrationSettingsTab.Templates}
|
||||
id={incident.alert_receive_channel.id}
|
||||
onHide={() =>
|
||||
this.setState({
|
||||
showIntegrationSettings: undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{showAttachIncidentForm && (
|
||||
<AttachIncidentForm
|
||||
id={id}
|
||||
onHide={() => {
|
||||
this.setState({
|
||||
showAttachIncidentForm: false,
|
||||
});
|
||||
}}
|
||||
onUpdate={this.update}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</PageErrorHandlingWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -210,7 +206,7 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
const showLinkTo = !incident.dependent_alert_groups.length && !incident.root_alert_group && !incident.resolved;
|
||||
|
||||
return (
|
||||
<Block withBackground>
|
||||
<Block withBackground className={cx('block')}>
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup className={cx('title')}>
|
||||
<PluginLink query={{ page: 'incidents', cursor, start, perpage }}>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React, { ReactElement, SyntheticEvent } from 'react';
|
||||
|
||||
import { Button, Icon, Tooltip, VerticalGroup, LoadingPlaceholder, HorizontalGroup } from '@grafana/ui';
|
||||
import { PluginPage } from 'PluginPage';
|
||||
import cn from 'classnames/bind';
|
||||
import { get } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
|
|
@ -20,7 +19,6 @@ import IncidentsFilters from 'containers/IncidentsFilters/IncidentsFilters';
|
|||
import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl';
|
||||
import { Alert, Alert as AlertType, AlertAction } from 'models/alertgroup/alertgroup.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { pages } from 'pages';
|
||||
import { getActionButtons, getIncidentStatusTag, renderRelatedUsers } from 'pages/incident/Incident.helpers';
|
||||
import { move } from 'state/helpers';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
|
|
@ -101,12 +99,10 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
|
||||
render() {
|
||||
return (
|
||||
<PluginPage pageNav={pages['incidents'].getPageNav()}>
|
||||
<div className={cx('root')}>
|
||||
{this.renderIncidentFilters()}
|
||||
{this.renderTable()}
|
||||
</div>
|
||||
</PluginPage>
|
||||
<div className={cx('root')}>
|
||||
{this.renderIncidentFilters()}
|
||||
{this.renderTable()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Button, LoadingPlaceholder, VerticalGroup } from '@grafana/ui';
|
||||
import { PluginPage } from 'PluginPage';
|
||||
import cn from 'classnames/bind';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
|
|
@ -24,7 +23,6 @@ import { IntegrationSettingsTab } from 'containers/IntegrationSettings/Integrati
|
|||
import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel';
|
||||
import { AlertReceiveChannelOption } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { pages } from 'pages';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
|
|
@ -131,121 +129,119 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
const searchResult = alertReceiveChannelStore.getSearchResult();
|
||||
|
||||
return (
|
||||
<PluginPage pageNav={pages['integrations'].getPageNav()}>
|
||||
<PageErrorHandlingWrapper
|
||||
errorData={errorData}
|
||||
objectName="integration"
|
||||
pageName="integrations"
|
||||
itemNotFoundMessage={`Integration with id=${query?.id} is not found. Please select integration from the list.`}
|
||||
>
|
||||
{() => (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('filters')}>
|
||||
<IntegrationsFilters value={integrationsFilters} onChange={this.handleIntegrationsFiltersChange} />
|
||||
<PageErrorHandlingWrapper
|
||||
errorData={errorData}
|
||||
objectName="integration"
|
||||
pageName="integrations"
|
||||
itemNotFoundMessage={`Integration with id=${query?.id} is not found. Please select integration from the list.`}
|
||||
>
|
||||
{() => (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('filters')}>
|
||||
<IntegrationsFilters value={integrationsFilters} onChange={this.handleIntegrationsFiltersChange} />
|
||||
</div>
|
||||
{searchResult?.length ? (
|
||||
<div className={cx('integrations')}>
|
||||
<div className={cx('integrationsList')}>
|
||||
<WithPermissionControl userAction={UserActions.IntegrationsWrite}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.setState({ showCreateIntegrationModal: true });
|
||||
}}
|
||||
icon="plus"
|
||||
className={cx('newIntegrationButton')}
|
||||
>
|
||||
New integration for receiving alerts
|
||||
</Button>
|
||||
</WithPermissionControl>
|
||||
<div className={cx('alert-receive-channels-list')}>
|
||||
<GList
|
||||
autoScroll
|
||||
selectedId={store.selectedAlertReceiveChannel}
|
||||
items={searchResult}
|
||||
itemKey="id"
|
||||
onSelect={this.handleAlertReceiveChannelSelect}
|
||||
>
|
||||
{(item) => (
|
||||
<AlertReceiveChannelCard
|
||||
id={item.id}
|
||||
onShowHeartbeatModal={() => {
|
||||
this.setState({
|
||||
alertReceiveChannelToShowSettings: item.id,
|
||||
integrationSettingsTab: IntegrationSettingsTab.Heartbeat,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</GList>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cx('alert-rules', 'alertRulesBorder')}>
|
||||
<AlertRules
|
||||
alertReceiveChannelId={store.selectedAlertReceiveChannel}
|
||||
onDelete={this.handleDeleteAlertReceiveChannel}
|
||||
onShowSettings={(integrationSettingsTab?: IntegrationSettingsTab) => {
|
||||
this.setState({
|
||||
alertReceiveChannelToShowSettings: store.selectedAlertReceiveChannel,
|
||||
integrationSettingsTab,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{searchResult?.length ? (
|
||||
<div className={cx('integrations')}>
|
||||
<div className={cx('integrationsList')}>
|
||||
) : searchResult ? (
|
||||
<Tutorial
|
||||
step={TutorialStep.Integrations}
|
||||
title={
|
||||
<VerticalGroup align="center" spacing="lg">
|
||||
<Text type="secondary">No integrations found. Review your filter and team settings.</Text>
|
||||
<WithPermissionControl userAction={UserActions.IntegrationsWrite}>
|
||||
<Button
|
||||
icon="plus"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
this.setState({ showCreateIntegrationModal: true });
|
||||
}}
|
||||
icon="plus"
|
||||
className={cx('newIntegrationButton')}
|
||||
>
|
||||
New integration for receiving alerts
|
||||
</Button>
|
||||
</WithPermissionControl>
|
||||
<div className={cx('alert-receive-channels-list')}>
|
||||
<GList
|
||||
autoScroll
|
||||
selectedId={store.selectedAlertReceiveChannel}
|
||||
items={searchResult}
|
||||
itemKey="id"
|
||||
onSelect={this.handleAlertReceiveChannelSelect}
|
||||
>
|
||||
{(item) => (
|
||||
<AlertReceiveChannelCard
|
||||
id={item.id}
|
||||
onShowHeartbeatModal={() => {
|
||||
this.setState({
|
||||
alertReceiveChannelToShowSettings: item.id,
|
||||
integrationSettingsTab: IntegrationSettingsTab.Heartbeat,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</GList>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cx('alert-rules', 'alertRulesBorder')}>
|
||||
<AlertRules
|
||||
alertReceiveChannelId={store.selectedAlertReceiveChannel}
|
||||
onDelete={this.handleDeleteAlertReceiveChannel}
|
||||
onShowSettings={(integrationSettingsTab?: IntegrationSettingsTab) => {
|
||||
this.setState({
|
||||
alertReceiveChannelToShowSettings: store.selectedAlertReceiveChannel,
|
||||
integrationSettingsTab,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : searchResult ? (
|
||||
<Tutorial
|
||||
step={TutorialStep.Integrations}
|
||||
title={
|
||||
<VerticalGroup align="center" spacing="lg">
|
||||
<Text type="secondary">No integrations found. Review your filter and team settings.</Text>
|
||||
<WithPermissionControl userAction={UserActions.IntegrationsWrite}>
|
||||
<Button
|
||||
icon="plus"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
this.setState({ showCreateIntegrationModal: true });
|
||||
}}
|
||||
>
|
||||
New integration for receiving alerts
|
||||
</Button>
|
||||
</WithPermissionControl>
|
||||
</VerticalGroup>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<LoadingPlaceholder text="Loading..." />
|
||||
)}
|
||||
</div>
|
||||
{alertReceiveChannelToShowSettings && (
|
||||
<IntegrationSettings
|
||||
onUpdate={() => {
|
||||
alertReceiveChannelStore.updateItem(alertReceiveChannelToShowSettings);
|
||||
}}
|
||||
startTab={integrationSettingsTab}
|
||||
id={alertReceiveChannelToShowSettings}
|
||||
onHide={() => {
|
||||
this.setState({
|
||||
alertReceiveChannelToShowSettings: undefined,
|
||||
integrationSettingsTab: undefined,
|
||||
});
|
||||
LocationHelper.update({ tab: undefined }, 'partial');
|
||||
}}
|
||||
</VerticalGroup>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<LoadingPlaceholder text="Loading..." />
|
||||
)}
|
||||
{showCreateIntegrationModal && (
|
||||
<CreateAlertReceiveChannelContainer
|
||||
onHide={() => {
|
||||
this.setState({ showCreateIntegrationModal: false });
|
||||
}}
|
||||
onCreate={this.handleCreateNewAlertReceiveChannel}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</PageErrorHandlingWrapper>
|
||||
</PluginPage>
|
||||
</div>
|
||||
{alertReceiveChannelToShowSettings && (
|
||||
<IntegrationSettings
|
||||
onUpdate={() => {
|
||||
alertReceiveChannelStore.updateItem(alertReceiveChannelToShowSettings);
|
||||
}}
|
||||
startTab={integrationSettingsTab}
|
||||
id={alertReceiveChannelToShowSettings}
|
||||
onHide={() => {
|
||||
this.setState({
|
||||
alertReceiveChannelToShowSettings: undefined,
|
||||
integrationSettingsTab: undefined,
|
||||
});
|
||||
LocationHelper.update({ tab: undefined }, 'partial');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showCreateIntegrationModal && (
|
||||
<CreateAlertReceiveChannelContainer
|
||||
onHide={() => {
|
||||
this.setState({ showCreateIntegrationModal: false });
|
||||
}}
|
||||
onCreate={this.handleCreateNewAlertReceiveChannel}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</PageErrorHandlingWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Button, VerticalGroup } from '@grafana/ui';
|
||||
import { PluginPage } from 'PluginPage';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
import moment from 'moment-timezone';
|
||||
|
|
@ -16,7 +15,6 @@ import { WithPermissionControl } from 'containers/WithPermissionControl/WithPerm
|
|||
import { getAlertReceiveChannelDisplayName } from 'models/alert_receive_channel/alert_receive_channel.helpers';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { Maintenance, MaintenanceMode, MaintenanceType } from 'models/maintenance/maintenance.types';
|
||||
import { pages } from 'pages';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
|
|
@ -117,7 +115,7 @@ class MaintenancePage extends React.Component<MaintenancePageProps, MaintenanceP
|
|||
];
|
||||
|
||||
return (
|
||||
<PluginPage pageNav={pages['maintenance'].getPageNav()}>
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<GTable
|
||||
emptyText={data ? 'No maintenances found' : 'Loading...'}
|
||||
|
|
@ -160,7 +158,7 @@ class MaintenancePage extends React.Component<MaintenancePageProps, MaintenanceP
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
</PluginPage>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Button, HorizontalGroup, Tag, Tooltip } from '@grafana/ui';
|
||||
import { PluginPage } from 'PluginPage';
|
||||
import cn from 'classnames/bind';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
|
|
@ -96,41 +95,39 @@ class OrganizationLogPage extends React.Component<OrganizationLogProps, Organiza
|
|||
const loading = !results;
|
||||
|
||||
return (
|
||||
<PluginPage>
|
||||
<div className={cx('root')}>
|
||||
<OrganizationLogFilters value={filters} onChange={this.handleChangeOrganizationLogFilters} />
|
||||
<GTable
|
||||
rowKey="id"
|
||||
title={() => (
|
||||
<div className={cx('header')}>
|
||||
<Text.Title className={cx('users-title')} level={3}>
|
||||
Organization Logs
|
||||
</Text.Title>
|
||||
<Button onClick={this.refresh} icon={loading ? 'fa fa-spinner' : 'sync'}>
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
showHeader={true}
|
||||
data={results}
|
||||
loading={loading}
|
||||
emptyText={results ? 'No logs found' : 'Loading...'}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
page,
|
||||
total: Math.ceil((total || 0) / ITEMS_PER_PAGE),
|
||||
onChange: this.handleChangePage,
|
||||
}}
|
||||
rowClassName={cx('align-top')}
|
||||
expandable={{
|
||||
expandedRowRender: this.renderFullDescription,
|
||||
expandRowByClick: true,
|
||||
expandedRowKeys: expandedLogsKeys,
|
||||
onExpandedRowsChange: this.handleExpandedRowsChange,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</PluginPage>
|
||||
<div className={cx('root')}>
|
||||
<OrganizationLogFilters value={filters} onChange={this.handleChangeOrganizationLogFilters} />
|
||||
<GTable
|
||||
rowKey="id"
|
||||
title={() => (
|
||||
<div className={cx('header')}>
|
||||
<Text.Title className={cx('users-title')} level={3}>
|
||||
Organization Logs
|
||||
</Text.Title>
|
||||
<Button onClick={this.refresh} icon={loading ? 'fa fa-spinner' : 'sync'}>
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
showHeader={true}
|
||||
data={results}
|
||||
loading={loading}
|
||||
emptyText={results ? 'No logs found' : 'Loading...'}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
page,
|
||||
total: Math.ceil((total || 0) / ITEMS_PER_PAGE),
|
||||
onChange: this.handleChangePage,
|
||||
}}
|
||||
rowClassName={cx('align-top')}
|
||||
expandable={{
|
||||
expandedRowRender: this.renderFullDescription,
|
||||
expandRowByClick: true,
|
||||
expandedRowKeys: expandedLogsKeys,
|
||||
onExpandedRowsChange: this.handleExpandedRowsChange,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Button, HorizontalGroup } from '@grafana/ui';
|
||||
import { PluginPage } from 'PluginPage';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
import LegacyNavHeading from 'navbar/LegacyNavHeading';
|
||||
|
|
@ -19,7 +18,6 @@ import OutgoingWebhookForm from 'containers/OutgoingWebhookForm/OutgoingWebhookF
|
|||
import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl';
|
||||
import { ActionDTO } from 'models/action';
|
||||
import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types';
|
||||
import { pages } from 'pages';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
|
|
@ -111,54 +109,52 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
|
|||
];
|
||||
|
||||
return (
|
||||
<PluginPage pageNav={pages['outgoing_webhooks'].getPageNav()}>
|
||||
<PageErrorHandlingWrapper
|
||||
errorData={errorData}
|
||||
objectName="outgoing webhook"
|
||||
pageName="outgoing_webhooks"
|
||||
itemNotFoundMessage={`Outgoing webhook with id=${query?.id} is not found. Please select outgoing webhook from the list.`}
|
||||
>
|
||||
{() => (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<GTable
|
||||
emptyText={webhooks ? 'No outgoing webhooks found' : 'Loading...'}
|
||||
title={() => (
|
||||
<div className={cx('header')}>
|
||||
<LegacyNavHeading>
|
||||
<Text.Title level={3}>Outgoing Webhooks</Text.Title>
|
||||
</LegacyNavHeading>
|
||||
<div className="u-pull-right">
|
||||
<PluginLink
|
||||
partial
|
||||
query={{ id: 'new' }}
|
||||
disabled={!isUserActionAllowed(UserActions.OutgoingWebhooksWrite)}
|
||||
>
|
||||
<WithPermissionControl userAction={UserActions.OutgoingWebhooksWrite}>
|
||||
<Button variant="primary" icon="plus">
|
||||
Create
|
||||
</Button>
|
||||
</WithPermissionControl>
|
||||
</PluginLink>
|
||||
</div>
|
||||
<PageErrorHandlingWrapper
|
||||
errorData={errorData}
|
||||
objectName="outgoing webhook"
|
||||
pageName="outgoing_webhooks"
|
||||
itemNotFoundMessage={`Outgoing webhook with id=${query?.id} is not found. Please select outgoing webhook from the list.`}
|
||||
>
|
||||
{() => (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<GTable
|
||||
emptyText={webhooks ? 'No outgoing webhooks found' : 'Loading...'}
|
||||
title={() => (
|
||||
<div className={cx('header')}>
|
||||
<LegacyNavHeading>
|
||||
<Text.Title level={3}>Outgoing Webhooks</Text.Title>
|
||||
</LegacyNavHeading>
|
||||
<div className="u-pull-right">
|
||||
<PluginLink
|
||||
partial
|
||||
query={{ id: 'new' }}
|
||||
disabled={!isUserActionAllowed(UserActions.OutgoingWebhooksWrite)}
|
||||
>
|
||||
<WithPermissionControl userAction={UserActions.OutgoingWebhooksWrite}>
|
||||
<Button variant="primary" icon="plus">
|
||||
Create
|
||||
</Button>
|
||||
</WithPermissionControl>
|
||||
</PluginLink>
|
||||
</div>
|
||||
)}
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
data={webhooks}
|
||||
/>
|
||||
</div>
|
||||
{outgoingWebhookIdToEdit && (
|
||||
<OutgoingWebhookForm
|
||||
id={outgoingWebhookIdToEdit}
|
||||
onUpdate={this.update}
|
||||
onHide={this.handleOutgoingWebhookFormHide}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</PageErrorHandlingWrapper>
|
||||
</PluginPage>
|
||||
</div>
|
||||
)}
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
data={webhooks}
|
||||
/>
|
||||
</div>
|
||||
{outgoingWebhookIdToEdit && (
|
||||
<OutgoingWebhookForm
|
||||
id={outgoingWebhookIdToEdit}
|
||||
onUpdate={this.update}
|
||||
onHide={this.handleOutgoingWebhookFormHide}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</PageErrorHandlingWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Button, HorizontalGroup, VerticalGroup, IconButton, ToolbarButton, Icon, Modal } from '@grafana/ui';
|
||||
import { PluginPage } from 'PluginPage';
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
|
|
@ -20,7 +19,6 @@ import ScheduleICalSettings from 'containers/ScheduleIcalLink/ScheduleIcalLink';
|
|||
import UsersTimezones from 'containers/UsersTimezones/UsersTimezones';
|
||||
import { Schedule, ScheduleType, Shift } from 'models/schedule/schedule.types';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { pages } from 'pages';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
|
|
@ -112,156 +110,154 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
shiftIdToShowOverridesForm;
|
||||
|
||||
return (
|
||||
<PluginPage pageNav={pages['schedule'].getPageNav()}>
|
||||
<PageErrorHandlingWrapper pageName="schedules">
|
||||
{() => (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<VerticalGroup spacing="lg">
|
||||
<div className={cx('header')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<PageErrorHandlingWrapper pageName="schedules">
|
||||
{() => (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<VerticalGroup spacing="lg">
|
||||
<div className={cx('header')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<PluginLink query={{ page: 'schedules' }}>
|
||||
<IconButton style={{ marginTop: '5px' }} name="arrow-left" size="xl" />
|
||||
</PluginLink>
|
||||
<Text.Title
|
||||
editable
|
||||
editModalTitle="Schedule name"
|
||||
level={2}
|
||||
onTextChange={this.handleNameChange}
|
||||
>
|
||||
{schedule?.name}
|
||||
</Text.Title>
|
||||
{schedule && <ScheduleWarning item={schedule} />}
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup spacing="lg">
|
||||
{users && (
|
||||
<HorizontalGroup>
|
||||
<Text type="secondary">Current timezone:</Text>
|
||||
<UserTimezoneSelect
|
||||
value={currentTimezone}
|
||||
users={users}
|
||||
onChange={this.handleTimezoneChange}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
)}
|
||||
<HorizontalGroup>
|
||||
<PluginLink query={{ page: 'schedules' }}>
|
||||
<IconButton style={{ marginTop: '5px' }} name="arrow-left" size="xl" />
|
||||
</PluginLink>
|
||||
<Text.Title
|
||||
editable
|
||||
editModalTitle="Schedule name"
|
||||
level={2}
|
||||
onTextChange={this.handleNameChange}
|
||||
>
|
||||
{schedule?.name}
|
||||
</Text.Title>
|
||||
{schedule && <ScheduleWarning item={schedule} />}
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup spacing="lg">
|
||||
{users && (
|
||||
<HorizontalGroup>
|
||||
<Text type="secondary">Current timezone:</Text>
|
||||
<UserTimezoneSelect
|
||||
value={currentTimezone}
|
||||
users={users}
|
||||
onChange={this.handleTimezoneChange}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
)}
|
||||
<HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
<Button variant="secondary" onClick={this.handleExportClick()}>
|
||||
Export
|
||||
</Button>
|
||||
|
||||
{(schedule?.type === ScheduleType.Ical || schedule?.type === ScheduleType.Calendar) && (
|
||||
<Button variant="secondary" onClick={this.handleReloadClick(scheduleId)}>
|
||||
Reload
|
||||
</Button>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
<ToolbarButton
|
||||
icon="cog"
|
||||
tooltip="Settings"
|
||||
onClick={() => {
|
||||
this.setState({ showEditForm: true });
|
||||
}}
|
||||
/>
|
||||
<WithConfirm>
|
||||
<ToolbarButton icon="trash-alt" tooltip="Delete" onClick={this.handleDelete} />
|
||||
</WithConfirm>
|
||||
|
||||
{(schedule?.type === ScheduleType.Ical || schedule?.type === ScheduleType.Calendar) && (
|
||||
<Button variant="secondary" onClick={this.handleReloadClick(scheduleId)}>
|
||||
Reload
|
||||
</Button>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
<ToolbarButton
|
||||
icon="cog"
|
||||
tooltip="Settings"
|
||||
onClick={() => {
|
||||
this.setState({ showEditForm: true });
|
||||
}}
|
||||
/>
|
||||
<WithConfirm>
|
||||
<ToolbarButton icon="trash-alt" tooltip="Delete" onClick={this.handleDelete} />
|
||||
</WithConfirm>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('users-timezones')}>
|
||||
<UsersTimezones
|
||||
scheduleId={scheduleId}
|
||||
startMoment={startMoment}
|
||||
onCallNow={schedule?.on_call_now || []}
|
||||
userIds={
|
||||
scheduleStore.relatedUsers[scheduleId] ? Object.keys(scheduleStore.relatedUsers[scheduleId]) : []
|
||||
}
|
||||
tz={currentTimezone}
|
||||
onTzChange={this.handleTimezoneChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={cx('rotations')}>
|
||||
<div className={cx('controls')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<Button variant="secondary" onClick={this.handleTodayClick}>
|
||||
Today
|
||||
</Button>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Button variant="secondary" onClick={this.handleLeftClick}>
|
||||
<Icon name="angle-left" />
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={this.handleRightClick}>
|
||||
<Icon name="angle-right" />
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
<Text.Title style={{ marginLeft: '8px' }} level={4} type="primary">
|
||||
{startMoment.format('DD MMM')} - {startMoment.add(6, 'day').format('DD MMM')}
|
||||
</Text.Title>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('users-timezones')}>
|
||||
<UsersTimezones
|
||||
scheduleId={scheduleId}
|
||||
startMoment={startMoment}
|
||||
onCallNow={schedule?.on_call_now || []}
|
||||
userIds={
|
||||
scheduleStore.relatedUsers[scheduleId]
|
||||
? Object.keys(scheduleStore.relatedUsers[scheduleId])
|
||||
: []
|
||||
}
|
||||
tz={currentTimezone}
|
||||
onTzChange={this.handleTimezoneChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={cx('rotations')}>
|
||||
<div className={cx('controls')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<Button variant="secondary" onClick={this.handleTodayClick}>
|
||||
Today
|
||||
</Button>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Button variant="secondary" onClick={this.handleLeftClick}>
|
||||
<Icon name="angle-left" />
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={this.handleRightClick}>
|
||||
<Icon name="angle-right" />
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
<Text.Title style={{ marginLeft: '8px' }} level={4} type="primary">
|
||||
{startMoment.format('DD MMM')} - {startMoment.add(6, 'day').format('DD MMM')}
|
||||
</Text.Title>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<ScheduleFinal
|
||||
scheduleId={scheduleId}
|
||||
currentTimezone={currentTimezone}
|
||||
startMoment={startMoment}
|
||||
onClick={this.handleShowForm}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Rotations
|
||||
scheduleId={scheduleId}
|
||||
currentTimezone={currentTimezone}
|
||||
startMoment={startMoment}
|
||||
onCreate={this.handleCreateRotation}
|
||||
onUpdate={this.handleUpdateRotation}
|
||||
onDelete={this.handleDeleteRotation}
|
||||
shiftIdToShowRotationForm={shiftIdToShowRotationForm}
|
||||
onShowRotationForm={this.handleShowRotationForm}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<ScheduleOverrides
|
||||
scheduleId={scheduleId}
|
||||
currentTimezone={currentTimezone}
|
||||
startMoment={startMoment}
|
||||
onCreate={this.handleCreateOverride}
|
||||
onUpdate={this.handleUpdateOverride}
|
||||
onDelete={this.handleDeleteOverride}
|
||||
shiftIdToShowRotationForm={shiftIdToShowOverridesForm}
|
||||
onShowRotationForm={this.handleShowOverridesForm}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
{showEditForm && (
|
||||
<ScheduleForm
|
||||
id={schedule.id}
|
||||
onUpdate={this.update}
|
||||
onHide={() => {
|
||||
this.setState({ showEditForm: false });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showScheduleICalSettings && (
|
||||
<Modal
|
||||
isOpen
|
||||
title="Schedule export"
|
||||
closeOnEscape
|
||||
onDismiss={() => this.setState({ showScheduleICalSettings: false })}
|
||||
>
|
||||
<ScheduleICalSettings id={scheduleId} />
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</PageErrorHandlingWrapper>
|
||||
</PluginPage>
|
||||
<ScheduleFinal
|
||||
scheduleId={scheduleId}
|
||||
currentTimezone={currentTimezone}
|
||||
startMoment={startMoment}
|
||||
onClick={this.handleShowForm}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Rotations
|
||||
scheduleId={scheduleId}
|
||||
currentTimezone={currentTimezone}
|
||||
startMoment={startMoment}
|
||||
onCreate={this.handleCreateRotation}
|
||||
onUpdate={this.handleUpdateRotation}
|
||||
onDelete={this.handleDeleteRotation}
|
||||
shiftIdToShowRotationForm={shiftIdToShowRotationForm}
|
||||
onShowRotationForm={this.handleShowRotationForm}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<ScheduleOverrides
|
||||
scheduleId={scheduleId}
|
||||
currentTimezone={currentTimezone}
|
||||
startMoment={startMoment}
|
||||
onCreate={this.handleCreateOverride}
|
||||
onUpdate={this.handleUpdateOverride}
|
||||
onDelete={this.handleDeleteOverride}
|
||||
shiftIdToShowRotationForm={shiftIdToShowOverridesForm}
|
||||
onShowRotationForm={this.handleShowOverridesForm}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
{showEditForm && (
|
||||
<ScheduleForm
|
||||
id={schedule.id}
|
||||
onUpdate={this.update}
|
||||
onHide={() => {
|
||||
this.setState({ showEditForm: false });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showScheduleICalSettings && (
|
||||
<Modal
|
||||
isOpen
|
||||
title="Schedule export"
|
||||
closeOnEscape
|
||||
onDismiss={() => this.setState({ showScheduleICalSettings: false })}
|
||||
>
|
||||
<ScheduleICalSettings id={scheduleId} />
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</PageErrorHandlingWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Button, HorizontalGroup, IconButton, LoadingPlaceholder, VerticalGroup } from '@grafana/ui';
|
||||
import { PluginPage } from 'PluginPage';
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
import { debounce } from 'lodash-es';
|
||||
|
|
@ -25,7 +24,6 @@ import { WithPermissionControl } from 'containers/WithPermissionControl/WithPerm
|
|||
import { Schedule, ScheduleType } from 'models/schedule/schedule.types';
|
||||
import { getSlackChannelName } from 'models/slack_channel/slack_channel.helpers';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { pages } from 'pages';
|
||||
import { getStartOfWeek } from 'pages/schedule/Schedule.helpers';
|
||||
import { WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
|
|
@ -135,7 +133,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
: undefined;
|
||||
|
||||
return (
|
||||
<PluginPage pageNav={pages['schedules'].getPageNav()}>
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup justify="space-between">
|
||||
|
|
@ -192,7 +190,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
</PluginPage>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Tab, TabsBar } from '@grafana/ui';
|
||||
import { PluginPage } from 'PluginPage';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
|
|
@ -35,11 +34,7 @@ class SettingsPage extends React.Component<SettingsPageProps, SettingsPageState>
|
|||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PluginPage pageNav={this.getMatchingPageNav()}>
|
||||
<div className={cx('root')}>{this.renderContent()}</div>
|
||||
</PluginPage>
|
||||
);
|
||||
return <div className={cx('root')}>{this.renderContent()}</div>;
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Button } from '@grafana/ui';
|
||||
import { PluginPage } from 'PluginPage';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
|
|
@ -17,13 +16,11 @@ const cx = cn.bind(styles);
|
|||
class Test extends React.Component<any, any> {
|
||||
render() {
|
||||
return (
|
||||
<PluginPage>
|
||||
<div className={cx('root')}>
|
||||
<WithPermissionControl userAction={UserActions.SchedulesWrite}>
|
||||
{(disabled) => <Button disabled={disabled}>Click me!</Button>}
|
||||
</WithPermissionControl>
|
||||
</div>
|
||||
</PluginPage>
|
||||
<div className={cx('root')}>
|
||||
<WithPermissionControl userAction={UserActions.SchedulesWrite}>
|
||||
{(disabled) => <Button disabled={disabled}>Click me!</Button>}
|
||||
</WithPermissionControl>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Alert, Button, HorizontalGroup, Icon, VerticalGroup } from '@grafana/ui';
|
||||
import { PluginPage } from 'PluginPage';
|
||||
import cn from 'classnames/bind';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
|
|
@ -20,7 +19,6 @@ import UsersFilters from 'components/UsersFilters/UsersFilters';
|
|||
import UserSettings from 'containers/UserSettings/UserSettings';
|
||||
import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl';
|
||||
import { User as UserType } from 'models/user/user.types';
|
||||
import { pages } from 'pages';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
|
|
@ -159,85 +157,83 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
const { count, results } = userStore.getSearchResult();
|
||||
|
||||
return (
|
||||
<PluginPage pageNav={pages['users'].getPageNav()}>
|
||||
<PageErrorHandlingWrapper
|
||||
errorData={errorData}
|
||||
objectName="user"
|
||||
pageName="users"
|
||||
itemNotFoundMessage={`User with id=${query?.id} is not found. Please select user from the list.`}
|
||||
>
|
||||
{() => (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('root', 'TEST-users-page')}>
|
||||
<div className={cx('users-header')}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline' }}>
|
||||
<div>
|
||||
<LegacyNavHeading>
|
||||
<Text.Title level={3}>Users</Text.Title>
|
||||
</LegacyNavHeading>
|
||||
<Text type="secondary">
|
||||
To manage permissions or add users, please visit{' '}
|
||||
<a href="/org/users">Grafana user management</a>
|
||||
</Text>
|
||||
</div>
|
||||
<PageErrorHandlingWrapper
|
||||
errorData={errorData}
|
||||
objectName="user"
|
||||
pageName="users"
|
||||
itemNotFoundMessage={`User with id=${query?.id} is not found. Please select user from the list.`}
|
||||
>
|
||||
{() => (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('root', 'TEST-users-page')}>
|
||||
<div className={cx('users-header')}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline' }}>
|
||||
<div>
|
||||
<LegacyNavHeading>
|
||||
<Text.Title level={3}>Users</Text.Title>
|
||||
</LegacyNavHeading>
|
||||
<Text type="secondary">
|
||||
To manage permissions or add users, please visit{' '}
|
||||
<a href="/org/users">Grafana user management</a>
|
||||
</Text>
|
||||
</div>
|
||||
<PluginLink partial query={{ id: 'me' }}>
|
||||
<Button variant="primary" icon="user">
|
||||
View my profile
|
||||
</Button>
|
||||
</PluginLink>
|
||||
</div>
|
||||
{isUserActionAllowed(UserActions.UserSettingsRead) ? (
|
||||
<>
|
||||
<div className={cx('user-filters-container')}>
|
||||
<UsersFilters
|
||||
className={cx('users-filters')}
|
||||
value={usersFilters}
|
||||
onChange={this.handleUsersFiltersChange}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="times"
|
||||
onClick={handleClear}
|
||||
className={cx('searchIntegrationClear')}
|
||||
>
|
||||
Clear filters
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<GTable
|
||||
emptyText={results ? 'No users found' : 'Loading...'}
|
||||
rowKey="pk"
|
||||
data={results}
|
||||
columns={columns}
|
||||
rowClassName={getUserRowClassNameFn(userPkToEdit, userStore.currentUserPk)}
|
||||
pagination={{
|
||||
page,
|
||||
total: Math.ceil((count || 0) / ITEMS_PER_PAGE),
|
||||
onChange: this.handleChangePage,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Alert
|
||||
/* @ts-ignore */
|
||||
title={
|
||||
<>
|
||||
You don't have enough permissions to view other users because you are not Admin.{' '}
|
||||
<PluginLink query={{ page: 'users', id: 'me' }}>Click here</PluginLink> to open your profile
|
||||
</>
|
||||
}
|
||||
severity="info"
|
||||
/>
|
||||
)}
|
||||
<PluginLink partial query={{ id: 'me' }}>
|
||||
<Button variant="primary" icon="user">
|
||||
View my profile
|
||||
</Button>
|
||||
</PluginLink>
|
||||
</div>
|
||||
{userPkToEdit && <UserSettings id={userPkToEdit} onHide={this.handleHideUserSettings} />}
|
||||
{isUserActionAllowed(UserActions.UserSettingsRead) ? (
|
||||
<>
|
||||
<div className={cx('user-filters-container')}>
|
||||
<UsersFilters
|
||||
className={cx('users-filters')}
|
||||
value={usersFilters}
|
||||
onChange={this.handleUsersFiltersChange}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="times"
|
||||
onClick={handleClear}
|
||||
className={cx('searchIntegrationClear')}
|
||||
>
|
||||
Clear filters
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<GTable
|
||||
emptyText={results ? 'No users found' : 'Loading...'}
|
||||
rowKey="pk"
|
||||
data={results}
|
||||
columns={columns}
|
||||
rowClassName={getUserRowClassNameFn(userPkToEdit, userStore.currentUserPk)}
|
||||
pagination={{
|
||||
page,
|
||||
total: Math.ceil((count || 0) / ITEMS_PER_PAGE),
|
||||
onChange: this.handleChangePage,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Alert
|
||||
/* @ts-ignore */
|
||||
title={
|
||||
<>
|
||||
You don't have enough permissions to view other users because you are not Admin.{' '}
|
||||
<PluginLink query={{ page: 'users', id: 'me' }}>Click here</PluginLink> to open your profile
|
||||
</>
|
||||
}
|
||||
severity="info"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</PageErrorHandlingWrapper>
|
||||
</PluginPage>
|
||||
{userPkToEdit && <UserSettings id={userPkToEdit} onHide={this.handleHideUserSettings} />}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</PageErrorHandlingWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { routes } from 'pages/routes';
|
|||
import { rootStore } from 'state';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { isUserActionAllowed } from 'utils/authorization';
|
||||
import { DEFAULT_PAGE } from 'utils/consts';
|
||||
import { useQueryParams, useQueryPath } from 'utils/hooks';
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
|
@ -49,7 +50,7 @@ export const GrafanaPluginRootPage = (props: AppRootProps) => (
|
|||
export const Root = observer((props: AppRootProps) => {
|
||||
const [didFinishLoading, setDidFinishLoading] = useState(false);
|
||||
const queryParams = useQueryParams();
|
||||
const page = queryParams.get('page');
|
||||
const page = queryParams.get('page') || DEFAULT_PAGE;
|
||||
const path = useQueryPath();
|
||||
|
||||
// Required to support grafana instances that use a custom `root_url`.
|
||||
|
|
@ -93,18 +94,15 @@ export const Root = observer((props: AppRootProps) => {
|
|||
{!isTopNavbar() && (
|
||||
<>
|
||||
<Header page={page} backendLicense={store.backendLicense} />
|
||||
<nav className="page-container">
|
||||
<LegacyNavTabsBar currentPage={page} />
|
||||
</nav>
|
||||
<LegacyNavTabsBar currentPage={page} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={classnames(
|
||||
{ 'page-container': !isTopNavbar() },
|
||||
{ 'page-body': !isTopNavbar() },
|
||||
'u-position-relative'
|
||||
)}
|
||||
className={classnames('u-position-relative', 'u-flex-grow-1', {
|
||||
'u-overflow-x-auto': !isTopNavbar(),
|
||||
'page-body': !isTopNavbar(),
|
||||
})}
|
||||
>
|
||||
{userHasAccess ? (
|
||||
<Page {...props} query={...getQueryParams()} path={pathWithoutLeadingSlash} store={store} />
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.u-overflow-x-auto {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.u-pull-right {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
|
@ -18,6 +22,10 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.u-height-100 {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.u-flex {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
@ -28,6 +36,10 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.u-flex-grow-1 {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.u-align-items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,27 @@
|
|||
import plugin from '../../package.json'; // eslint-disable-line
|
||||
|
||||
// Navbar
|
||||
export const APP_TITLE = 'Grafana OnCall';
|
||||
export const APP_SUBTITLE = `Developer-friendly incident response (${plugin?.version})`;
|
||||
|
||||
// License
|
||||
export const GRAFANA_LICENSE_OSS = 'OpenSource';
|
||||
|
||||
// Reusable breakpoint sizes
|
||||
export const BREAKPOINT_TABS = 1024;
|
||||
|
||||
// Default redirect page
|
||||
export const DEFAULT_PAGE = 'incidents';
|
||||
|
||||
// Environment options list for onCallApiUrl
|
||||
export const ONCALL_PROD = 'https://oncall-prod-us-central-0.grafana.net/oncall';
|
||||
export const ONCALL_OPS = 'https://oncall-ops-us-east-0.grafana.net/oncall';
|
||||
export const ONCALL_DEV = 'https://oncall-dev-us-central-0.grafana.net/oncall';
|
||||
|
||||
// Faro
|
||||
export const FARO_ENDPOINT_DEV =
|
||||
'https://faro-collector-prod-us-central-0.grafana.net/collect/fb03e474a96cf867f4a34590c002984c';
|
||||
export const FARO_ENDPOINT_OPS =
|
||||
'https://faro-collector-prod-us-central-0.grafana.net/collect/40ccaafad6b71aa90fc53c3b0a1adb31';
|
||||
export const FARO_ENDPOINT_PROD =
|
||||
'https://faro-collector-prod-us-central-0.grafana.net/collect/03a11ed03c3af04dcfc3be9755f2b053';
|
||||
|
|
|
|||
|
|
@ -1,89 +0,0 @@
|
|||
import 'jest/matchMedia.ts';
|
||||
import { describe, test } from '@jest/globals';
|
||||
|
||||
import FaroHelper from 'utils/faro';
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
jest.mock('@grafana/faro-web-sdk', () => ({
|
||||
initializeFaro: jest.fn().mockReturnValue({
|
||||
api: {
|
||||
pushLog: jest.fn(),
|
||||
},
|
||||
}),
|
||||
getWebInstrumentations: () => [],
|
||||
}));
|
||||
|
||||
jest.mock('@grafana/faro-web-tracing', () => ({
|
||||
TracingInstrumentation: jest.fn(),
|
||||
}));
|
||||
jest.mock('@opentelemetry/instrumentation-document-load', () => ({
|
||||
DocumentLoadInstrumentation: jest.fn(),
|
||||
}));
|
||||
jest.mock('@opentelemetry/instrumentation-fetch', () => ({
|
||||
FetchInstrumentation: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Faro', () => {
|
||||
const OLD_ENV = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
process.env = { ...OLD_ENV };
|
||||
FaroHelper.faro = undefined;
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const getDefaultValues = () => ({
|
||||
faroUrl: 'localhost:12345/collect',
|
||||
apiKey: 'secret',
|
||||
enabled: 'true',
|
||||
});
|
||||
|
||||
const getProcessEnv = (config: { faroUrl?: string; apiKey?: string; enabled?: string } = {}) => {
|
||||
const configObject = {
|
||||
...getDefaultValues(),
|
||||
...config,
|
||||
};
|
||||
|
||||
const { faroUrl, apiKey, enabled } = configObject;
|
||||
|
||||
return {
|
||||
FARO_URL: faroUrl,
|
||||
FARO_API_KEY: apiKey,
|
||||
FARO_ENABLED: enabled,
|
||||
};
|
||||
};
|
||||
|
||||
test('It initializes without api key', () => {
|
||||
process.env = getProcessEnv({ apiKey: '' });
|
||||
const faro = FaroHelper.initializeFaro();
|
||||
expect(faro).toBeDefined();
|
||||
});
|
||||
|
||||
test('It initializes faro ENABLED === true', () => {
|
||||
process.env = getProcessEnv();
|
||||
const faro = FaroHelper.initializeFaro();
|
||||
|
||||
expect(faro).toBeDefined();
|
||||
expect(faro.api.pushLog).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('It does not initialize faro if ENABLED != true', () => {
|
||||
process.env = getProcessEnv({ enabled: 'some-other-value-here' });
|
||||
const faro = FaroHelper.initializeFaro();
|
||||
expect(faro).toBeUndefined();
|
||||
});
|
||||
|
||||
test('It skips initializing if values are missing', () => {
|
||||
let faro;
|
||||
|
||||
process.env = getProcessEnv({ faroUrl: undefined });
|
||||
faro = FaroHelper.initializeFaro();
|
||||
expect(faro).toBeUndefined();
|
||||
|
||||
process.env = getProcessEnv({ enabled: undefined });
|
||||
faro = FaroHelper.initializeFaro();
|
||||
expect(faro).toBeUndefined();
|
||||
});
|
||||
});
|
||||
65
grafana-plugin/src/utils/faro.test.tsx
Normal file
65
grafana-plugin/src/utils/faro.test.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import 'jest/matchMedia.ts';
|
||||
import { describe, test } from '@jest/globals';
|
||||
|
||||
import FaroHelper from 'utils/faro';
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import { ONCALL_DEV, ONCALL_OPS, ONCALL_PROD } from './consts';
|
||||
|
||||
const ErrorMock = jest.spyOn(window, 'Error');
|
||||
|
||||
jest.mock('@grafana/faro-web-sdk', () => ({
|
||||
initializeFaro: jest.fn().mockReturnValue({
|
||||
api: {
|
||||
pushLog: jest.fn(),
|
||||
},
|
||||
}),
|
||||
getWebInstrumentations: () => [],
|
||||
}));
|
||||
|
||||
jest.mock('@grafana/faro-web-tracing', () => ({
|
||||
TracingInstrumentation: jest.fn(),
|
||||
}));
|
||||
jest.mock('@opentelemetry/instrumentation-document-load', () => ({
|
||||
DocumentLoadInstrumentation: jest.fn(),
|
||||
}));
|
||||
jest.mock('@opentelemetry/instrumentation-fetch', () => ({
|
||||
FetchInstrumentation: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Faro', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
FaroHelper.faro = undefined;
|
||||
});
|
||||
|
||||
test.each([ONCALL_DEV, ONCALL_OPS, ONCALL_PROD])('It initializes faro for environment %s', (onCallApiUrl) => {
|
||||
const faro = FaroHelper.initializeFaro(onCallApiUrl);
|
||||
expect(faro).toBeDefined();
|
||||
expect(ErrorMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test.each(['https://test.com', 'http://localhost:3000'])(
|
||||
'It fails initializing for dummy environment values',
|
||||
(onCallApiUrl) => {
|
||||
const faro = FaroHelper.initializeFaro(onCallApiUrl);
|
||||
expect(faro).toBeUndefined();
|
||||
}
|
||||
);
|
||||
|
||||
test('It does not reinitialize faro instance if already initialized', () => {
|
||||
const instance = FaroHelper.initializeFaro(ONCALL_DEV);
|
||||
expect(instance).toBeDefined();
|
||||
|
||||
const result = FaroHelper.initializeFaro(ONCALL_PROD);
|
||||
expect(result).toBeUndefined();
|
||||
expect(FaroHelper.faro).toBe(instance);
|
||||
});
|
||||
|
||||
test('Initializer throws error for wrong env value', () => {
|
||||
const faro = FaroHelper.initializeFaro('https://test.com');
|
||||
expect(ErrorMock).toHaveBeenCalledWith(`No match found for given onCallApiUrl = https://test.com`);
|
||||
expect(faro).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -6,47 +6,45 @@ import { UserInteractionInstrumentation } from '@opentelemetry/instrumentation-u
|
|||
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
|
||||
|
||||
import plugin from '../../package.json'; // eslint-disable-line
|
||||
import {
|
||||
FARO_ENDPOINT_DEV,
|
||||
FARO_ENDPOINT_OPS,
|
||||
FARO_ENDPOINT_PROD,
|
||||
ONCALL_DEV,
|
||||
ONCALL_OPS,
|
||||
ONCALL_PROD,
|
||||
} from './consts';
|
||||
|
||||
const IGNORE_URLS = [/^((?!\/{0,1}a\/grafana\-oncall\-app\\).)*$/];
|
||||
|
||||
interface FaroConfig {
|
||||
url: string;
|
||||
enabled: boolean;
|
||||
environment: string;
|
||||
export function getAppNameUrlPair(onCallApiUrl: string): { appName: string; url: string } {
|
||||
const baseName = 'grafana-oncall';
|
||||
|
||||
switch (onCallApiUrl) {
|
||||
case ONCALL_DEV:
|
||||
return { appName: `${baseName}-dev`, url: FARO_ENDPOINT_DEV };
|
||||
case ONCALL_OPS:
|
||||
return { appName: `${baseName}-ops`, url: FARO_ENDPOINT_OPS };
|
||||
case ONCALL_PROD:
|
||||
return { appName: `${baseName}-prod`, url: FARO_ENDPOINT_PROD };
|
||||
default:
|
||||
throw new Error(`No match found for given onCallApiUrl = ${onCallApiUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
class FaroHelper {
|
||||
faro: Faro;
|
||||
|
||||
initializeFaro(onCallApiUrl: string) {
|
||||
const faroConfig: FaroConfig = {
|
||||
url: 'https://faro-collector-prod-us-central-0.grafana.net/collect/f3a038193e7802cf47531ca94cfbada7',
|
||||
enabled: false,
|
||||
environment: undefined,
|
||||
};
|
||||
|
||||
if (onCallApiUrl === 'https://oncall-prod-us-central-0.grafana.net/oncall') {
|
||||
faroConfig.enabled = true;
|
||||
faroConfig.environment = 'prod';
|
||||
} else if (onCallApiUrl === 'https://oncall-ops-us-east-0.grafana.net/oncall') {
|
||||
faroConfig.enabled = true;
|
||||
faroConfig.environment = 'ops';
|
||||
} else if (onCallApiUrl === 'https://oncall-dev-us-central-0.grafana.net/oncall') {
|
||||
faroConfig.enabled = true;
|
||||
faroConfig.environment = 'dev';
|
||||
} else {
|
||||
// This opensource, don't send traces
|
||||
/* faroConfig.enabled = true;
|
||||
faroConfig.environment = 'local'; */
|
||||
}
|
||||
|
||||
if (!faroConfig?.enabled || !faroConfig?.url || this.faro) {
|
||||
if (this.faro) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const { appName, url } = getAppNameUrlPair(onCallApiUrl);
|
||||
|
||||
const faroOptions = {
|
||||
url: faroConfig.url,
|
||||
url: url,
|
||||
isolate: true,
|
||||
instrumentations: [
|
||||
...getWebInstrumentations({
|
||||
|
|
@ -63,15 +61,14 @@ class FaroHelper {
|
|||
],
|
||||
session: (window as any).__PRELOADED_STATE__?.faro?.session,
|
||||
app: {
|
||||
name: 'grafana-oncall',
|
||||
name: appName,
|
||||
version: plugin?.version,
|
||||
environment: faroConfig.environment,
|
||||
},
|
||||
};
|
||||
|
||||
this.faro = initializeFaro(faroOptions);
|
||||
|
||||
this.faro.api.pushLog([`Faro was initialized for ${faroConfig.environment}`]);
|
||||
this.faro.api.pushLog([`Faro was initialized for ${appName}`]);
|
||||
} catch (ex) {}
|
||||
|
||||
return this.faro;
|
||||
|
|
|
|||
|
|
@ -10913,11 +10913,16 @@ prettier-linter-helpers@^1.0.0:
|
|||
dependencies:
|
||||
fast-diff "^1.1.2"
|
||||
|
||||
prettier@2.7.1, prettier@^2.7.1:
|
||||
prettier@2.7.1:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"
|
||||
integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==
|
||||
|
||||
prettier@^2.8.2:
|
||||
version "2.8.2"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.2.tgz#c4ea1b5b454d7c4b59966db2e06ed7eec5dfd160"
|
||||
integrity sha512-BtRV9BcncDyI2tsuS19zzhzoxD8Dh8LiCx7j7tHzrkz8GFXAexeWFdi22mjE1d16dftH2qNaytVxqiRTGlMfpw==
|
||||
|
||||
pretty-error@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue