commit
bb7d0aaf21
24 changed files with 244 additions and 111 deletions
|
|
@ -40,7 +40,7 @@ steps:
|
|||
image: plugins/gcs
|
||||
settings:
|
||||
acl: allUsers:READER
|
||||
source: grafana-plugin/ci/dist/grafana-oncall-app-${DRONE_TAG}.zip
|
||||
source: grafana-plugin/grafana-oncall-app-${DRONE_TAG}.zip
|
||||
target: grafana-oncall-app/releases/grafana-oncall-app-${DRONE_TAG}.zip
|
||||
token:
|
||||
from_secret: gcs_oncall_publisher_key
|
||||
|
|
@ -385,4 +385,6 @@ name: cloud_access_policy_token
|
|||
|
||||
---
|
||||
kind: signature
|
||||
hmac: 198b7c7d2c94fc5698b22a722e7748181990207755cf1778b2290137e262518c
|
||||
hmac: d541ed21fc2472272c6772e246aaf1a2606db112b4e72a44bc4530831e9ca4d3
|
||||
|
||||
...
|
||||
|
|
|
|||
27
.github/workflows/e2e-tests.yml
vendored
27
.github/workflows/e2e-tests.yml
vendored
|
|
@ -33,6 +33,11 @@ jobs:
|
|||
# this will allow us to run more backend containers and parralelize the tests)
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
name: "Grafana: ${{ inputs.grafana-image-tag }}"
|
||||
environment:
|
||||
name: github-pages
|
||||
permissions:
|
||||
id-token: write
|
||||
pages: "write"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
|
@ -222,9 +227,25 @@ jobs:
|
|||
with:
|
||||
important-workloads: deploy/oncall-ci-grafana
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- name: Setup Pages
|
||||
if: failure()
|
||||
uses: actions/configure-pages@v2
|
||||
|
||||
- name: Upload artifact
|
||||
if: failure()
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
with:
|
||||
name: playwright-report-${{ inputs.grafana-image-tag }}
|
||||
path: ./grafana-plugin/playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
if: failure()
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v3
|
||||
with:
|
||||
preview: true
|
||||
|
||||
- name: Linked Github Page
|
||||
if: failure()
|
||||
run: |
|
||||
echo "Test report has been deployed to [GitHub Pages](https://grafana.github.io/oncall/) :rocket:" \
|
||||
>> $GITHUB_STEP_SUMMARY
|
||||
|
|
|
|||
|
|
@ -5,7 +5,12 @@ 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).
|
||||
|
||||
## Unreleased
|
||||
## v1.3.108 (2024-02-28)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fetch selected value of notify schedule in escalation policy, fixes ([#3966](https://github.com/grafana/oncall/issues/3966))
|
||||
([#3969](https://github.com/grafana/oncall/pull/3969))
|
||||
|
||||
## v1.3.107 (2024-02-27)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
import { test as base, Browser, Fixtures, Page, TestInfo } from '@playwright/test';
|
||||
import { test as base, Browser, Fixtures, Page } from '@playwright/test';
|
||||
|
||||
import { VIEWER_USER_STORAGE_STATE, EDITOR_USER_STORAGE_STATE, ADMIN_USER_STORAGE_STATE } from '../playwright.config';
|
||||
|
||||
import { GRAFANA_ADMIN_USERNAME, GRAFANA_EDITOR_USERNAME, GRAFANA_VIEWER_USERNAME } from './utils/constants';
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export class BaseRolePage {
|
||||
page: Page;
|
||||
userName: string;
|
||||
|
|
@ -39,55 +36,29 @@ interface TestFixtures extends Fixtures {
|
|||
|
||||
interface WorkerFixtures extends Fixtures {}
|
||||
|
||||
/**
|
||||
* NOTE: currently videos are not generated automatically because of how we generate a browserContext within our
|
||||
* auth fixtures (which is how Playwright suggested setting up multi-role authnz tests..). There's a GitHub
|
||||
* Issue here that tracks this issue https://github.com/microsoft/playwright/issues/14813
|
||||
*
|
||||
* Here's a temporary workaround on this, which is what this function does
|
||||
* https://github.com/microsoft/playwright/issues/14813#issuecomment-1582499142
|
||||
*/
|
||||
const _recordTestVideo = async (
|
||||
const setContextForPage = async (
|
||||
browser: Browser,
|
||||
use: (r: BaseRolePage) => Promise<void>,
|
||||
testInfo: TestInfo,
|
||||
storageStateLocation: string,
|
||||
RolePage: BaseRolePageType
|
||||
) => {
|
||||
const videoDir = path.join(testInfo.outputPath(), 'videos');
|
||||
|
||||
const context = await browser.newContext({
|
||||
storageState: storageStateLocation,
|
||||
recordVideo: { dir: videoDir },
|
||||
});
|
||||
const page = new RolePage(await context.newPage());
|
||||
|
||||
try {
|
||||
await use(page);
|
||||
} finally {
|
||||
await context.close();
|
||||
const videoFiles = fs.readdirSync(videoDir);
|
||||
|
||||
if (videoFiles.length > 0) {
|
||||
for (let i = videoFiles.length; i > 0; i--) {
|
||||
let videoFile = path.join(videoDir, videoFiles[i - 1]);
|
||||
await testInfo.attach('video', { path: videoFile });
|
||||
}
|
||||
}
|
||||
}
|
||||
await use(page);
|
||||
};
|
||||
|
||||
export * from '@playwright/test';
|
||||
export const test = base.extend<TestFixtures, WorkerFixtures>({
|
||||
viewerRolePage: ({ browser }, use, testInfo) =>
|
||||
_recordTestVideo(browser, use, testInfo, VIEWER_USER_STORAGE_STATE, ViewerRolePage),
|
||||
editorRolePage: async ({ browser }, use, testInfo) =>
|
||||
_recordTestVideo(browser, use, testInfo, EDITOR_USER_STORAGE_STATE, EditorRolePage),
|
||||
adminRolePage: async ({ browser }, use, testInfo) =>
|
||||
_recordTestVideo(browser, use, testInfo, ADMIN_USER_STORAGE_STATE, AdminRolePage),
|
||||
/**
|
||||
* add back this fixture once this bug is fixed
|
||||
* https://github.com/microsoft/playwright/issues/29608
|
||||
*/
|
||||
// currentGrafanaVersion: ({}, use) => use('9.0.0'),
|
||||
viewerRolePage: async ({ browser }, use) =>
|
||||
setContextForPage(browser, use, VIEWER_USER_STORAGE_STATE, ViewerRolePage),
|
||||
editorRolePage: async ({ browser }, use) =>
|
||||
setContextForPage(browser, use, EDITOR_USER_STORAGE_STATE, EditorRolePage),
|
||||
adminRolePage: async ({ browser }, use) => setContextForPage(browser, use, ADMIN_USER_STORAGE_STATE, AdminRolePage),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import semver from 'semver';
|
||||
|
||||
import { test, expect } from '../fixtures';
|
||||
import { resolveFiringAlert } from '../utils/alertGroup';
|
||||
import { createEscalationChain, EscalationStep } from '../utils/escalationChain';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { test, expect, Page, Locator } from '../fixtures';
|
||||
import { verifyThatAlertGroupIsRoutedCorrectlyButNotEscalated } from '../utils/alertGroup';
|
||||
import { EscalationStep, createEscalationChain } from '../utils/escalationChain';
|
||||
import { clickButton, generateRandomValue, selectDropdownValue } from '../utils/forms';
|
||||
import { generateRandomValue, selectDropdownValue } from '../utils/forms';
|
||||
import {
|
||||
assignEscalationChainToIntegration,
|
||||
createIntegration,
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export default defineConfig({
|
|||
// baseURL: 'http://localhost:3000',
|
||||
|
||||
trace: 'on',
|
||||
video: 'on',
|
||||
video: 'off',
|
||||
headless: true,
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import styles from './GTable.module.css';
|
|||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
export interface Props<RecordType = unknown> extends TableProps<RecordType> {
|
||||
export interface GTableProps<RecordType = unknown> extends TableProps<RecordType> {
|
||||
pagination?: {
|
||||
page: number;
|
||||
total: number;
|
||||
|
|
@ -31,7 +31,7 @@ export interface Props<RecordType = unknown> extends TableProps<RecordType> {
|
|||
showHeader?: boolean;
|
||||
}
|
||||
|
||||
export const GTable = <RT extends DefaultRecordType = DefaultRecordType>(props: Props<RT>): ReactElement => {
|
||||
export const GTable = <RT extends DefaultRecordType = DefaultRecordType>(props: GTableProps<RT>): ReactElement => {
|
||||
const {
|
||||
columns: columnsProp,
|
||||
data,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import React, { ChangeEvent } from 'react';
|
|||
import { SelectableValue } from '@grafana/data';
|
||||
import { Button, Input, Select, IconButton } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
import moment from 'moment-timezone';
|
||||
import { SortableElement } from 'react-sortable-hoc';
|
||||
import reactStringReplace from 'react-string-replace';
|
||||
|
|
@ -56,7 +57,14 @@ export interface EscalationPolicyProps extends ElementSortableProps {
|
|||
isSlackInstalled: boolean;
|
||||
}
|
||||
|
||||
@observer
|
||||
class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
||||
componentDidMount() {
|
||||
if (this.props.data.notify_schedule) {
|
||||
this.props.store.scheduleStore.loadItem(this.props.data.notify_schedule);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, escalationChoices, number, isDisabled, backgroundClassName, backgroundHexNumber } = this.props;
|
||||
const { id, step, is_final } = data;
|
||||
|
|
|
|||
|
|
@ -39,5 +39,6 @@ export const getStyles = () => ({
|
|||
}),
|
||||
disabledBadge: css({
|
||||
wordBreak: 'keep-all',
|
||||
marginLeft: '8px',
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -39,7 +39,10 @@ export const AlertReceiveChannelCard = observer((props: AlertReceiveChannelCardP
|
|||
|
||||
const heartbeatStatus = Boolean(heartbeat?.status);
|
||||
|
||||
const integration = AlertReceiveChannelHelper.getIntegration(alertReceiveChannelStore, alertReceiveChannel);
|
||||
const integration = AlertReceiveChannelHelper.getIntegrationSelectOption(
|
||||
alertReceiveChannelStore,
|
||||
alertReceiveChannel
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx('root')}>
|
||||
|
|
|
|||
|
|
@ -239,6 +239,7 @@ export const IntegrationForm = observer((props: IntegrationFormProps) => {
|
|||
|
||||
if (!IntegrationHelper.isSpecificIntegration(selectedOption.value, 'grafana_alerting')) {
|
||||
pushHistory(response.id);
|
||||
return;
|
||||
}
|
||||
|
||||
await (data.is_existing
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export class AlertReceiveChannelHelper {
|
|||
: undefined;
|
||||
}
|
||||
|
||||
static getIntegration(
|
||||
static getIntegrationSelectOption(
|
||||
store: AlertReceiveChannelStore,
|
||||
alertReceiveChannel: Partial<ApiSchemas['AlertReceiveChannel'] | ApiSchemas['FastAlertReceiveChannel']>
|
||||
): SelectOption {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { AlertTemplatesDTO } from 'models/alert_templates/alert_templates';
|
|||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
||||
import { Heartbeat } from 'models/heartbeat/heartbeat.types';
|
||||
import { ActionKey } from 'models/loader/action-keys';
|
||||
import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types';
|
||||
import { makeRequest } from 'network/network';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
|
|
@ -12,7 +13,7 @@ import { operations } from 'network/oncall-api/autogenerated-api.types';
|
|||
import { onCallApi } from 'network/oncall-api/http-client';
|
||||
import { move } from 'state/helpers';
|
||||
import { RootBaseStore } from 'state/rootBaseStore/RootBaseStore';
|
||||
import { WithGlobalNotification } from 'utils/decorators';
|
||||
import { AutoLoadingState, WithGlobalNotification } from 'utils/decorators';
|
||||
import { OmitReadonlyMembers } from 'utils/types';
|
||||
|
||||
import { AlertReceiveChannelCounters, ContactPoint } from './alert_receive_channel.types';
|
||||
|
|
@ -124,20 +125,23 @@ export class AlertReceiveChannelStore {
|
|||
return results;
|
||||
}
|
||||
|
||||
@AutoLoadingState(ActionKey.FETCH_INTEGRATIONS)
|
||||
async fetchPaginatedItems({
|
||||
filters,
|
||||
page = 1,
|
||||
shouldFetchCounters = false,
|
||||
invalidateFn = undefined,
|
||||
perpage,
|
||||
}: {
|
||||
filters: operations['alert_receive_channels_list']['parameters']['query'];
|
||||
page: number;
|
||||
shouldFetchCounters: boolean;
|
||||
invalidateFn: () => boolean;
|
||||
page?: number;
|
||||
shouldFetchCounters?: boolean;
|
||||
invalidateFn?: () => boolean;
|
||||
perpage?: number;
|
||||
}) {
|
||||
const {
|
||||
data: { count, results, page_size },
|
||||
} = await onCallApi().GET('/alert_receive_channels/', { params: { query: { ...filters, page } } });
|
||||
} = await onCallApi().GET('/alert_receive_channels/', { params: { query: { ...filters, page, perpage } } });
|
||||
|
||||
if (invalidateFn?.()) {
|
||||
return undefined;
|
||||
|
|
@ -173,6 +177,10 @@ export class AlertReceiveChannelStore {
|
|||
return results;
|
||||
}
|
||||
|
||||
resetPaginatedResults() {
|
||||
this.paginatedSearchResult = {};
|
||||
}
|
||||
|
||||
populateHearbeats(alertReceiveChannels: Array<ApiSchemas['AlertReceiveChannelPolymorphic']>) {
|
||||
const heartbeats = alertReceiveChannels.reduce(
|
||||
(acc: any, alertReceiveChannel: ApiSchemas['AlertReceiveChannel']) => {
|
||||
|
|
|
|||
|
|
@ -8,4 +8,5 @@ export enum ActionKey {
|
|||
FETCH_INCIDENTS_POLLING = 'FETCH_INCIDENTS_POLLING',
|
||||
FETCH_INCIDENTS_AND_STATS = 'FETCH_INCIDENTS_AND_STATS',
|
||||
UPDATE_FILTERS_AND_FETCH_INCIDENTS = 'UPDATE_FILTERS_AND_FETCH_INCIDENTS',
|
||||
FETCH_INTEGRATIONS = 'FETCH_INTEGRATIONS',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
} = this.props;
|
||||
const { alerts } = store.alertGroupStore;
|
||||
const incident = alerts.get(id);
|
||||
const integration = AlertReceiveChannelHelper.getIntegration(
|
||||
const integration = AlertReceiveChannelHelper.getIntegrationSelectOption(
|
||||
store.alertReceiveChannelStore,
|
||||
incident.alert_receive_channel
|
||||
);
|
||||
|
|
|
|||
|
|
@ -630,7 +630,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
const {
|
||||
store: { alertReceiveChannelStore },
|
||||
} = this.props;
|
||||
const integration = AlertReceiveChannelHelper.getIntegration(
|
||||
const integration = AlertReceiveChannelHelper.getIntegrationSelectOption(
|
||||
alertReceiveChannelStore,
|
||||
record.alert_receive_channel
|
||||
);
|
||||
|
|
|
|||
|
|
@ -71,10 +71,6 @@ export const IntegrationHelper = {
|
|||
return hasSlack || hasTelegram || isMSTeamsInstalled;
|
||||
},
|
||||
|
||||
fetchChatOps(store: RootStore): Promise<void> {
|
||||
return store.msteamsChannelStore.updateMSTeamsChannels();
|
||||
},
|
||||
|
||||
getChatOpsChannels(channelFilter: ChannelFilter, store: RootStore): Array<{ name: string; icon: IconName }> {
|
||||
const channels: Array<{ name: string; icon: IconName }> = [];
|
||||
const telegram = Object.keys(store.telegramChannelStore.items).map((k) => store.telegramChannelStore.items[k]);
|
||||
|
|
|
|||
|
|
@ -152,7 +152,10 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
|
|||
);
|
||||
}
|
||||
|
||||
const integration = AlertReceiveChannelHelper.getIntegration(alertReceiveChannelStore, alertReceiveChannel);
|
||||
const integration = AlertReceiveChannelHelper.getIntegrationSelectOption(
|
||||
alertReceiveChannelStore,
|
||||
alertReceiveChannel
|
||||
);
|
||||
const alertReceiveChannelCounter = alertReceiveChannelStore.counters[id];
|
||||
const isLegacyIntegration = integration && (integration?.value as string).toLowerCase().startsWith('legacy_');
|
||||
const contactPoints = alertReceiveChannelStore.connectedContactPoints?.[alertReceiveChannel.id];
|
||||
|
|
@ -750,8 +753,7 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
|
|||
|
||||
async loadData() {
|
||||
const {
|
||||
store,
|
||||
store: { alertReceiveChannelStore },
|
||||
store: { alertReceiveChannelStore, msteamsChannelStore, hasFeature },
|
||||
match: {
|
||||
params: { id },
|
||||
},
|
||||
|
|
@ -771,7 +773,9 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
|
|||
}
|
||||
|
||||
promises.push(alertReceiveChannelStore.fetchTemplates(id));
|
||||
promises.push(IntegrationHelper.fetchChatOps(store));
|
||||
if (hasFeature(AppFeature.MsTeams)) {
|
||||
promises.push(msteamsChannelStore.updateMSTeamsChannels());
|
||||
}
|
||||
promises.push(alertReceiveChannelStore.fetchCountersForIntegration(id));
|
||||
|
||||
await Promise.all(promises)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,65 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { Icon, Input, Modal, useStyles2 } from '@grafana/ui';
|
||||
import { Button, HorizontalGroup, Icon, Input, Modal, useStyles2 } from '@grafana/ui';
|
||||
import cn from 'classnames';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
|
||||
import { ActionKey } from 'models/loader/action-keys';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { useCommonStyles, useIsLoading } from 'utils/hooks';
|
||||
|
||||
import ConnectedIntegrationsTable from './ConnectedIntegrationsTable';
|
||||
import { getStyles } from './OutgoingTab.styles';
|
||||
|
||||
export const ConnectIntegrationModal = ({ onDismiss }: { onDismiss: () => void }) => {
|
||||
const DEBOUNCE_MS = 500;
|
||||
|
||||
export const ConnectIntegrationModal = observer(({ onDismiss }: { onDismiss: () => void }) => {
|
||||
const { alertReceiveChannelStore } = useStore();
|
||||
const isLoading = useIsLoading(ActionKey.FETCH_INTEGRATIONS);
|
||||
const commonStyles = useCommonStyles();
|
||||
const [selectedIntegrations, setSelectedIntegrations] = useState<Array<ApiSchemas['AlertReceiveChannel']>>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const { count, results, page_size } = AlertReceiveChannelHelper.getPaginatedSearchResult(alertReceiveChannelStore);
|
||||
|
||||
useEffect(() => {
|
||||
fetchItems();
|
||||
return alertReceiveChannelStore.resetPaginatedResults;
|
||||
}, [page]);
|
||||
|
||||
const fetchItems = async (search?: string) => {
|
||||
await alertReceiveChannelStore.fetchPaginatedItems({
|
||||
filters: { search },
|
||||
perpage: 10,
|
||||
page,
|
||||
});
|
||||
};
|
||||
|
||||
const onChange = (integration: ApiSchemas['AlertReceiveChannel'], checked) => {
|
||||
if (checked) {
|
||||
setSelectedIntegrations((integrations) => [...integrations, integration]);
|
||||
} else {
|
||||
setSelectedIntegrations((integrations) => integrations.filter(({ id }) => id !== integration.id));
|
||||
}
|
||||
};
|
||||
|
||||
const onConnect = () => {};
|
||||
|
||||
const debouncedSearch = debounce(fetchItems, DEBOUNCE_MS);
|
||||
|
||||
const onSearchInputChange = (searchTerm: string) => {
|
||||
debouncedSearch(searchTerm);
|
||||
};
|
||||
|
||||
const onChangePage = (page: number) => {
|
||||
setPage(page);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen
|
||||
|
|
@ -17,13 +67,37 @@ export const ConnectIntegrationModal = ({ onDismiss }: { onDismiss: () => void }
|
|||
closeOnBackdropClick={false}
|
||||
closeOnEscape
|
||||
onDismiss={onDismiss}
|
||||
contentClassName={styles.connectIntegrationModalContent}
|
||||
>
|
||||
<Input
|
||||
className={styles.searchIntegrationsInput}
|
||||
suffix={<Icon name="search" />}
|
||||
placeholder="Search integrations..."
|
||||
onChange={(e) => onSearchInputChange(e.currentTarget.value)}
|
||||
/>
|
||||
<ConnectedIntegrationsTable />
|
||||
<ConnectedIntegrationsTable
|
||||
selectable
|
||||
onChange={onChange}
|
||||
tableProps={{
|
||||
data: results,
|
||||
pagination: {
|
||||
page,
|
||||
total: results ? Math.ceil((count || 0) / page_size) : 0,
|
||||
onChange: onChangePage,
|
||||
},
|
||||
emptyText: isLoading ? 'Loading...' : 'No integrations found',
|
||||
}}
|
||||
/>
|
||||
<div className={cn(commonStyles.bottomDrawerButtons, styles.connectIntegrationModalButtons)}>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<Button variant="secondary" onClick={onDismiss}>
|
||||
Close
|
||||
</Button>
|
||||
<Button variant="primary" onClick={onConnect} disabled={!selectedIntegrations?.length}>
|
||||
Connect
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,57 +1,78 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { HorizontalGroup, Tooltip, Icon, useStyles2, IconButton, Switch } from '@grafana/ui';
|
||||
import { HorizontalGroup, Tooltip, Icon, useStyles2, IconButton, Switch, Checkbox } from '@grafana/ui';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import { GTable } from 'components/GTable/GTable';
|
||||
import { GTable, GTableProps } from 'components/GTable/GTable';
|
||||
import { IntegrationLogoWithTitle } from 'components/IntegrationLogo/IntegrationLogoWithTitle';
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import { getStyles } from './OutgoingTab.styles';
|
||||
|
||||
interface ConnectedIntegrationsTableProps {
|
||||
allowDelete?: boolean;
|
||||
allowBacksync?: boolean;
|
||||
selectable?: boolean;
|
||||
onChange?: (integration: ApiSchemas['AlertReceiveChannel'], checked: boolean) => void;
|
||||
tableProps: GTableProps;
|
||||
}
|
||||
|
||||
const ConnectedIntegrationsTable: FC<ConnectedIntegrationsTableProps> = (props) => {
|
||||
const FAKE_INTEGRATIONS = [{ a: 'a' }];
|
||||
const ConnectedIntegrationsTable: FC<ConnectedIntegrationsTableProps> = observer(
|
||||
({ selectable, allowDelete, onChange, tableProps, allowBacksync }) => {
|
||||
const { alertReceiveChannelStore } = useStore();
|
||||
|
||||
return (
|
||||
<GTable
|
||||
emptyText={FAKE_INTEGRATIONS ? 'No integrations found' : 'Loading...'}
|
||||
rowKey="id"
|
||||
columns={getColumns(props)}
|
||||
data={FAKE_INTEGRATIONS}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const columns = [
|
||||
...(selectable
|
||||
? [
|
||||
{
|
||||
width: '5%',
|
||||
render: (integration: ApiSchemas['AlertReceiveChannel']) => (
|
||||
<Checkbox onChange={(event) => onChange(integration, event.currentTarget.checked)} />
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
width: '45%',
|
||||
title: <Text type="secondary">Integration name</Text>,
|
||||
dataIndex: 'verbal_name',
|
||||
render: (name: string) => name,
|
||||
},
|
||||
{
|
||||
width: '55%',
|
||||
title: <Text type="secondary">Type</Text>,
|
||||
render: (integration: ApiSchemas['AlertReceiveChannel']) => (
|
||||
<IntegrationLogoWithTitle
|
||||
integration={AlertReceiveChannelHelper.getIntegrationSelectOption(alertReceiveChannelStore, integration)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...(allowBacksync
|
||||
? [
|
||||
{
|
||||
title: (
|
||||
<HorizontalGroup>
|
||||
<Text type="secondary">Backsync</Text>
|
||||
<Tooltip content={<>Switch on to start sending data from other integrations</>}>
|
||||
<Icon name={'info-circle'} />
|
||||
</Tooltip>
|
||||
</HorizontalGroup>
|
||||
),
|
||||
render: BacksyncSwitcher,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
render: () => <ActionsColumn allowDelete={allowDelete} />,
|
||||
},
|
||||
];
|
||||
|
||||
const getColumns = ({ allowDelete }: ConnectedIntegrationsTableProps) => [
|
||||
{
|
||||
width: '45%',
|
||||
title: <Text type="secondary">Integration name</Text>,
|
||||
dataIndex: 'trigger_type_name',
|
||||
render: () => <>Some integration name</>,
|
||||
},
|
||||
{
|
||||
width: '55%',
|
||||
title: <Text type="secondary">Type</Text>,
|
||||
render: () => <IntegrationLogoWithTitle integration={{ value: 'elastalert', display_name: 'ElastAlerts' }} />,
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<HorizontalGroup>
|
||||
<Text type="secondary">Backsync</Text>
|
||||
<Tooltip content={<>Switch on to start sending data from other integrations</>}>
|
||||
<Icon name={'info-circle'} />
|
||||
</Tooltip>
|
||||
</HorizontalGroup>
|
||||
),
|
||||
render: BacksyncSwitcher,
|
||||
},
|
||||
{
|
||||
render: () => <ActionsColumn allowDelete={allowDelete} />,
|
||||
},
|
||||
];
|
||||
return <GTable rowKey="id" columns={columns} {...tableProps} />;
|
||||
}
|
||||
);
|
||||
|
||||
const BacksyncSwitcher = () => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export const OtherIntegrations = observer(() => {
|
|||
</Button>
|
||||
</HorizontalGroup>
|
||||
}
|
||||
content={<ConnectedIntegrationsTable allowDelete />}
|
||||
content={<ConnectedIntegrationsTable allowDelete allowBacksync tableProps={{ data: [] }} />}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -58,6 +58,9 @@ export const getStyles = (theme: GrafanaTheme2) => ({
|
|||
backsyncColumn: css({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
'& label': {
|
||||
position: 'relative',
|
||||
},
|
||||
}),
|
||||
triggerTemplateWrapper: css({
|
||||
position: 'relative',
|
||||
|
|
@ -77,4 +80,10 @@ export const getStyles = (theme: GrafanaTheme2) => ({
|
|||
tabsWrapper: css({
|
||||
padding: '16px 16px 0 8px',
|
||||
}),
|
||||
connectIntegrationModalContent: css({
|
||||
paddingBottom: 0,
|
||||
}),
|
||||
connectIntegrationModalButtons: css({
|
||||
marginTop: '50px',
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -122,6 +122,10 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.store.alertReceiveChannelStore.resetPaginatedResults();
|
||||
}
|
||||
|
||||
parseQueryParams = async () => {
|
||||
const {
|
||||
store,
|
||||
|
|
@ -353,7 +357,10 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
|
||||
renderDatasource(item: ApiSchemas['AlertReceiveChannel'], alertReceiveChannelStore: AlertReceiveChannelStore) {
|
||||
const alertReceiveChannel = alertReceiveChannelStore.items[item.id];
|
||||
const integration = AlertReceiveChannelHelper.getIntegration(alertReceiveChannelStore, alertReceiveChannel);
|
||||
const integration = AlertReceiveChannelHelper.getIntegrationSelectOption(
|
||||
alertReceiveChannelStore,
|
||||
alertReceiveChannel
|
||||
);
|
||||
const isLegacyIntegration = (integration?.value as string)?.toLowerCase().startsWith('legacy_');
|
||||
|
||||
if (isLegacyIntegration) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue