Merge pull request #1117 from grafana/dev

Dev to main v1.1.15
This commit is contained in:
Rares Mardare 2023-01-10 11:10:33 +02:00 committed by GitHub
commit 49789098d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 853 additions and 893 deletions

View file

@ -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 }}

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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:

View file

@ -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
)

View file

@ -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"},

View file

@ -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",

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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')}>

View file

@ -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 */
}

View file

@ -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];

View file

@ -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%;
}

View file

@ -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>

View file

@ -1,3 +1,4 @@
.root {
min-width: 1500px;
overflow-x: auto;
white-space: nowrap;
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -6,6 +6,10 @@
flex-grow: 1;
}
.block {
padding: 0 0 20px 0;
}
.payload-subtitle {
margin-bottom: var(--title-marginBottom);
}

View file

@ -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 }}>

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
</>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
</>
);
}

View file

@ -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() {

View file

@ -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>
);
}
}

View file

@ -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>
);
}

View file

@ -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} />

View file

@ -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;
}

View file

@ -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';

View file

@ -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();
});
});

View 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();
});
});

View file

@ -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;

View file

@ -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"