mirror of
https://github.com/grafana/grafana.git
synced 2025-02-09 23:16:16 -06:00
Gops: Add configuration tracker on the existing IRM page (#85838)
* WIP * add configure irm componet and pass it to Nav * add check if theres datasource for Alerts * update incident steps links * uncomment done property in the interface * Fix not having data sources in store and removing warning using wrong Icon name type * call incidents api in done step * Show checked steps with an icon instead of a badge * Implemnt check contact point ready in essentials * Fix codeowners and prettify file * Check if there is one oncall integration in at least one contact point * Refactor * Check for oncall integration being created * Add the test oncall integration action * Do not hide any card in irm page, and refactor * Refactor: move hooks to a separate file * Implement oncall chatops checks * update incidents hook * Show new irm page only on cloud and for admins * fix prettier in irmhooks * remove unused import * fix prettier in irm hooks * fix axios method * Add progress bar on essentials * Update texts and some styles * Refactor * fix api call * fix check is plugin is installed * fix async call * fix lint * Do not show check icon when done field is undefined in a step * refactor * Add test for Essentials * check if incident plugin installed * call incidents api to get steps * add the new api to get config * fix prettier * memoize the api call * fix lint * add proper api call * check if response is valid * fix typo * use state to save the values * fix lint * fix response schema * fix prettier * update incident steps copy * udapte texts in respond tooltips * Fix confiure IRM route check * Fix logic for the data source check: check if there is a data source that is alerting compatible * Use existing header prop in NavLandingPage instead of creating a new prop * fix wrong updated file * Update logic for default contact point check and update some links * Update texts and show only one item for oncall integration with alerting checks * Update texts following suggestions in the doc * Fix getting default contact point and update oncall link for slack tab * Update texts, buttons and checks following last meeting action items * remove unnecessary component drawer * Track interactions: user open or close essentials drawer * Refactor * remove unnecessary createMonitoringLogger for tracking irm events * remove unnecessary style * refactor * refactor * Add fallback links and labels for action buttons when step is done * Update irm card styles * remove extra space after progress bar numbers * remove progress bar border * Address pr review comments * remove unnecessary AlertmanagerProvider * fix logic behind default contact point check * update test * Address pr review comments part1 * add aria and properties role for Progressbar * Reorganize hooks into separate folders/files for each app * move done field to the step level * Handle empty dropdown options * Handle loading state * Update tooltip for connecting alerting to oncall * Use RTKQ for incidents * handle loading for oncall hooks * refactor getting configuration for each app * fix incident rtkq query to be a query instead of mutation * refactor: rename variable * Address some nits in the review --------- Co-authored-by: reemtariqq <reem.tariq@grafana.com>
This commit is contained in:
parent
367af27d7a
commit
ca0dae6812
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -283,6 +283,7 @@
|
||||
/pkg/services/sqlstore/migrations/ualert/ @grafana/alerting-backend-product
|
||||
/pkg/tests/api/alerting/ @grafana/alerting-backend-product
|
||||
/public/app/features/alerting/ @grafana/alerting-frontend
|
||||
/public/app/features/gops/ @grafana/alerting-frontend
|
||||
|
||||
# Library Services
|
||||
/pkg/services/libraryelements/ @grafana/dashboards-squad
|
||||
|
23
public/app/features/alerting/unified/api/incidentsApi.ts
Normal file
23
public/app/features/alerting/unified/api/incidentsApi.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { SupportedPlugin } from '../types/pluginBridges';
|
||||
|
||||
import { alertingApi } from './alertingApi';
|
||||
|
||||
interface IncidentsPluginConfigDto {
|
||||
isChatOpsInstalled: boolean;
|
||||
isIncidentCreated: boolean;
|
||||
}
|
||||
|
||||
const getProxyApiUrl = (path: string) =>
|
||||
`/api/plugins/grafana-incident-app/resources/${SupportedPlugin.Incident}${path}`;
|
||||
|
||||
export const incidentsApi = alertingApi.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
getIncidentsPluginConfig: build.query<IncidentsPluginConfigDto, void>({
|
||||
query: (integration) => ({
|
||||
url: getProxyApiUrl('/api/internal/v1/organization/config-checks/'),
|
||||
method: 'POST',
|
||||
showErrorAlert: false,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
@ -33,6 +33,11 @@ export interface CreateIntegrationDTO {
|
||||
verbal_name: string;
|
||||
}
|
||||
|
||||
export interface OnCallConfigChecks {
|
||||
is_chatops_connected: boolean;
|
||||
is_integration_chatops_connected: boolean;
|
||||
}
|
||||
|
||||
const getProxyApiUrl = (path: string) => `/api/plugin-proxy/${SupportedPlugin.OnCall}${path}`;
|
||||
|
||||
export const onCallApi = alertingApi.injectEndpoints({
|
||||
@ -79,6 +84,12 @@ export const onCallApi = alertingApi.injectEndpoints({
|
||||
showErrorAlert: false,
|
||||
}),
|
||||
}),
|
||||
onCallConfigChecks: build.query<OnCallConfigChecks, void>({
|
||||
query: () => ({
|
||||
url: getProxyApiUrl('/api/internal/v1/organization/config-checks/'),
|
||||
showErrorAlert: false,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { contextSrv, contextSrv as ctx } from 'app/core/services/context_srv';
|
||||
import { contextSrv as ctx } from 'app/core/services/context_srv';
|
||||
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
|
||||
@ -9,6 +9,7 @@ import { alertmanagerApi } from '../api/alertmanagerApi';
|
||||
import { useAlertmanager } from '../state/AlertmanagerContext';
|
||||
import { getInstancesPermissions, getNotificationsPermissions, getRulesPermissions } from '../utils/access-control';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
import { isAdmin } from '../utils/misc';
|
||||
import { isFederatedRuleGroup, isGrafanaRulerRule, isPluginProvidedRule } from '../utils/rules';
|
||||
|
||||
import { useIsRuleEditable } from './useIsRuleEditable';
|
||||
@ -197,9 +198,6 @@ export function useAllAlertmanagerAbilities(): Abilities<AlertmanagerAction> {
|
||||
const notificationsPermissions = getNotificationsPermissions(selectedAlertmanager!);
|
||||
const instancePermissions = getInstancesPermissions(selectedAlertmanager!);
|
||||
|
||||
//we need to know user role to determine if they can view autogenerated policy tree
|
||||
const isAdmin = contextSrv.hasRole('Admin') || contextSrv.isGrafanaAdmin;
|
||||
|
||||
// list out all of the abilities, and if the user has permissions to perform them
|
||||
const abilities: Abilities<AlertmanagerAction> = {
|
||||
// -- configuration --
|
||||
@ -236,7 +234,7 @@ export function useAllAlertmanagerAbilities(): Abilities<AlertmanagerAction> {
|
||||
isGrafanaFlavoredAlertmanager,
|
||||
notificationsPermissions.provisioning.readSecrets
|
||||
),
|
||||
[AlertmanagerAction.ViewAutogeneratedPolicyTree]: [isGrafanaFlavoredAlertmanager, isAdmin],
|
||||
[AlertmanagerAction.ViewAutogeneratedPolicyTree]: [isGrafanaFlavoredAlertmanager, isAdmin()],
|
||||
// -- silences --
|
||||
// for now, all supported Alertmanager flavors have API endpoints for managing silences
|
||||
[AlertmanagerAction.CreateSilence]: toAbility(AlwaysSupported, instancePermissions.create),
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { sortBy } from 'lodash';
|
||||
|
||||
import { UrlQueryMap, Labels } from '@grafana/data';
|
||||
import { Labels, UrlQueryMap } from '@grafana/data';
|
||||
import { GrafanaEdition } from '@grafana/data/src/types/config';
|
||||
import { config, isFetchError } from '@grafana/runtime';
|
||||
import { DataSourceRef } from '@grafana/schema';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { escapePathSeparators } from 'app/features/alerting/unified/utils/rule-id';
|
||||
import { alertInstanceKey, isGrafanaRulerRule } from 'app/features/alerting/unified/utils/rules';
|
||||
import { SortOrder } from 'app/plugins/panel/alertlist/types';
|
||||
@ -222,6 +223,10 @@ export function isOpenSourceEdition() {
|
||||
return buildInfo.edition === GrafanaEdition.OpenSource;
|
||||
}
|
||||
|
||||
export function isAdmin() {
|
||||
return contextSrv.hasRole('Admin') || contextSrv.isGrafanaAdmin;
|
||||
}
|
||||
|
||||
export function isLocalDevEnv() {
|
||||
const buildInfo = config.buildInfo;
|
||||
return buildInfo.env === 'development';
|
||||
|
18
public/app/features/gops/configuration-tracker/Analytics.ts
Normal file
18
public/app/features/gops/configuration-tracker/Analytics.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { reportInteraction } from '@grafana/runtime/src';
|
||||
|
||||
export enum IRMInteractionNames {
|
||||
ViewIRMMainPage = 'grafana_irm_configuration_tracker_main_page_view',
|
||||
OpenEssentials = 'grafana_irm_configuration_tracker_essentials_open',
|
||||
CloseEssentials = 'grafana_irm_configuration_tracker_essentials_closed',
|
||||
}
|
||||
|
||||
export interface ConfigurationTrackerContext {
|
||||
essentialStepsDone: number;
|
||||
essentialStepsToDo: number;
|
||||
}
|
||||
export function trackIrmConfigurationTrackerEvent(
|
||||
interactionName: IRMInteractionNames,
|
||||
payload: ConfigurationTrackerContext
|
||||
) {
|
||||
reportInteraction(interactionName, { ...payload });
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { alertRuleApi } from 'app/features/alerting/unified/api/alertRuleApi';
|
||||
import { alertmanagerApi } from 'app/features/alerting/unified/api/alertmanagerApi';
|
||||
import { ReceiverTypes } from 'app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/onCall';
|
||||
import { useDataSourceFeatures } from 'app/features/alerting/unified/hooks/useCombinedRule';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { Receiver } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
export function useIsCreateAlertRuleDone() {
|
||||
const [fetchRulerRules, { data, isLoading }] = alertRuleApi.endpoints.rulerRules.useLazyQuery({
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
});
|
||||
|
||||
const { dsFeatures, isLoadingDsFeatures } = useDataSourceFeatures(GRAFANA_RULES_SOURCE_NAME);
|
||||
const rulerConfig = dsFeatures?.rulerConfig;
|
||||
|
||||
useEffect(() => {
|
||||
rulerConfig && fetchRulerRules({ rulerConfig });
|
||||
}, [rulerConfig, fetchRulerRules]);
|
||||
|
||||
const rules = data
|
||||
? Object.entries(data).flatMap(([_, groupDto]) => {
|
||||
return groupDto.flatMap((group) => group.rules);
|
||||
})
|
||||
: [];
|
||||
const isDone = rules.length > 0;
|
||||
return { isDone, isLoading: isLoading || isLoadingDsFeatures };
|
||||
}
|
||||
|
||||
export function isOnCallContactPointReady(contactPoints: Receiver[]) {
|
||||
return contactPoints.some((contactPoint: Receiver) =>
|
||||
contactPoint.grafana_managed_receiver_configs?.some((receiver) => receiver.type === ReceiverTypes.OnCall)
|
||||
);
|
||||
}
|
||||
|
||||
export function useGetContactPoints() {
|
||||
const alertmanagerConfiguration = alertmanagerApi.endpoints.getAlertmanagerConfiguration.useQuery(
|
||||
GRAFANA_RULES_SOURCE_NAME,
|
||||
{
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
refetchOnMountOrArgChange: true,
|
||||
}
|
||||
);
|
||||
|
||||
const contactPoints = alertmanagerConfiguration.data?.alertmanager_config?.receivers ?? [];
|
||||
return { contactPoints, isLoading: alertmanagerConfiguration.isLoading };
|
||||
}
|
||||
|
||||
export function useGetDefaultContactPoint() {
|
||||
const alertmanagerConfiguration = alertmanagerApi.endpoints.getAlertmanagerConfiguration.useQuery(
|
||||
GRAFANA_RULES_SOURCE_NAME,
|
||||
{
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
refetchOnMountOrArgChange: true,
|
||||
}
|
||||
);
|
||||
|
||||
const defaultContactpoint = alertmanagerConfiguration.data?.alertmanager_config?.route?.receiver ?? '';
|
||||
return { defaultContactpoint, isLoading: alertmanagerConfiguration.isLoading };
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import { Receiver } from 'app/plugins/datasource/alertmanager/types';
|
||||
const DEFAULT_EMAIL = '<example@email.com>';
|
||||
|
||||
export function isContactPointReady(defaultContactPoint: string, contactPoints: Receiver[]) {
|
||||
// We consider the contact point ready if the default contact has the address filled
|
||||
|
||||
const defaultEmailUpdated = contactPoints.some(
|
||||
(contactPoint: Receiver) =>
|
||||
contactPoint.name === defaultContactPoint &&
|
||||
contactPoint.grafana_managed_receiver_configs?.some(
|
||||
(receiver) => receiver.name === defaultContactPoint && receiver.settings?.address !== DEFAULT_EMAIL
|
||||
)
|
||||
);
|
||||
return defaultEmailUpdated;
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
// ConfigCard.tsx
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, Icon, LoadingPlaceholder, Stack, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { IrmCardConfiguration } from './ConfigureIRM';
|
||||
import { ProgressBar, StepsStatus } from './ProgressBar';
|
||||
|
||||
interface ConfigCardProps {
|
||||
config: IrmCardConfiguration;
|
||||
handleActionClick: (id: number, isDone: boolean | undefined) => void;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function ConfigCard({ config, handleActionClick, isLoading = false }: ConfigCardProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
return (
|
||||
<Stack direction={'column'} gap={1} justifyContent={'space-around'}>
|
||||
<div className={styles.cardContent}>
|
||||
<Stack direction={'column'} gap={1}>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" gap={1}>
|
||||
<Stack direction={'row'} gap={1} alignItems={'center'}>
|
||||
{config.title}
|
||||
{config.titleIcon && <Icon name={config.titleIcon} />}
|
||||
{/* Only show check icon when not loading */}
|
||||
{config.isDone && !isLoading && <Icon name="check-circle" color="green" size="lg" />}
|
||||
</Stack>
|
||||
{config.stepsDone && config.totalStepsToDo && !isLoading && (
|
||||
<Stack direction="row" gap={0.5}>
|
||||
<StepsStatus stepsDone={config.stepsDone} totalStepsToDo={config.totalStepsToDo} />
|
||||
complete
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
<Stack direction={'column'}>
|
||||
{!isLoading ? config.description : <LoadingPlaceholder text="Loading configuration...." />}
|
||||
{/* Only show ProgressBar when not loading */}
|
||||
{!isLoading && config.stepsDone && config.totalStepsToDo && (
|
||||
<ProgressBar stepsDone={config.stepsDone} totalStepsToDo={config.totalStepsToDo} />
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack direction={'row'} gap={1} justifyContent={'flex-start'} alignItems={'flex-end'}>
|
||||
<Button variant="secondary" onClick={() => handleActionClick(config.id, config.isDone)}>
|
||||
{config.actionButtonTitle}
|
||||
</Button>
|
||||
</Stack>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
cardTitle: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
}),
|
||||
cardContent: css({
|
||||
background: theme.colors.background.secondary,
|
||||
padding: theme.spacing(2),
|
||||
borderRadius: theme.shape.radius.default,
|
||||
height: '100%',
|
||||
gap: theme.spacing(1),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
}),
|
||||
});
|
@ -0,0 +1,128 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { IconName, Text, useStyles2 } from '@grafana/ui';
|
||||
import { getFirstCompatibleDataSource } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { DATASOURCES_ROUTES } from 'app/features/datasources/constants';
|
||||
|
||||
import { IRMInteractionNames, trackIrmConfigurationTrackerEvent } from '../Analytics';
|
||||
import { useGetConfigurationForUI, useGetEssentialsConfiguration } from '../irmHooks';
|
||||
|
||||
import { ConfigCard } from './ConfigCard';
|
||||
import { Essentials } from './Essentials';
|
||||
export interface IrmCardConfiguration {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
actionButtonTitle: string;
|
||||
isDone?: boolean;
|
||||
stepsDone?: number;
|
||||
totalStepsToDo?: number;
|
||||
titleIcon?: IconName;
|
||||
}
|
||||
|
||||
export enum ConfigurationStepsEnum {
|
||||
CONNECT_DATASOURCE,
|
||||
ESSENTIALS,
|
||||
}
|
||||
|
||||
export interface DataSourceConfigurationData {
|
||||
dataSourceCompatibleWithAlerting: boolean;
|
||||
}
|
||||
function useGetDataSourceConfiguration(): DataSourceConfigurationData {
|
||||
return {
|
||||
dataSourceCompatibleWithAlerting: Boolean(getFirstCompatibleDataSource()),
|
||||
};
|
||||
}
|
||||
|
||||
export function ConfigureIRM() {
|
||||
const styles = useStyles2(getStyles);
|
||||
const history = useHistory();
|
||||
|
||||
// track only once when the component is mounted
|
||||
useEffect(() => {
|
||||
trackIrmConfigurationTrackerEvent(IRMInteractionNames.ViewIRMMainPage, {
|
||||
essentialStepsDone: 0,
|
||||
essentialStepsToDo: 0,
|
||||
});
|
||||
}, []);
|
||||
|
||||
// get all the configuration data
|
||||
const dataSourceConfigurationData = useGetDataSourceConfiguration();
|
||||
const essentialsConfigurationData = useGetEssentialsConfiguration();
|
||||
const configuration: IrmCardConfiguration[] = useGetConfigurationForUI({
|
||||
dataSourceConfigurationData,
|
||||
essentialsConfigurationData,
|
||||
});
|
||||
|
||||
const [essentialsOpen, setEssentialsOpen] = useState(false);
|
||||
|
||||
const handleActionClick = (configID: number, isDone?: boolean) => {
|
||||
switch (configID) {
|
||||
case ConfigurationStepsEnum.CONNECT_DATASOURCE:
|
||||
if (isDone) {
|
||||
history.push(DATASOURCES_ROUTES.List);
|
||||
} else {
|
||||
history.push(DATASOURCES_ROUTES.New);
|
||||
}
|
||||
break;
|
||||
case ConfigurationStepsEnum.ESSENTIALS:
|
||||
setEssentialsOpen(true);
|
||||
trackIrmConfigurationTrackerEvent(IRMInteractionNames.OpenEssentials, {
|
||||
essentialStepsDone: essentialsConfigurationData.stepsDone,
|
||||
essentialStepsToDo: essentialsConfigurationData.totalStepsToDo,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
function onCloseEssentials() {
|
||||
setEssentialsOpen(false);
|
||||
trackIrmConfigurationTrackerEvent(IRMInteractionNames.CloseEssentials, {
|
||||
essentialStepsDone: essentialsConfigurationData.stepsDone,
|
||||
essentialStepsToDo: essentialsConfigurationData.totalStepsToDo,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text element="h4" variant="h4">
|
||||
Configure
|
||||
</Text>
|
||||
<section className={styles.container}>
|
||||
{configuration.map((config) => (
|
||||
<ConfigCard
|
||||
key={config.id}
|
||||
config={config}
|
||||
handleActionClick={handleActionClick}
|
||||
isLoading={essentialsConfigurationData.isLoading}
|
||||
/>
|
||||
))}
|
||||
{essentialsOpen && (
|
||||
<Essentials
|
||||
onClose={onCloseEssentials}
|
||||
essentialsConfig={essentialsConfigurationData.essentialContent}
|
||||
stepsDone={essentialsConfigurationData.stepsDone}
|
||||
totalStepsToDo={essentialsConfigurationData.totalStepsToDo}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
<Text element="h4" variant="h4">
|
||||
IRM apps
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
container: css({
|
||||
marginBottom: 0,
|
||||
display: 'grid',
|
||||
gap: theme.spacing(3),
|
||||
gridTemplateColumns: ' 1fr 1fr',
|
||||
}),
|
||||
});
|
@ -0,0 +1,128 @@
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { byRole } from 'testing-library-selector';
|
||||
|
||||
import { SectionsDto } from '../irmHooks';
|
||||
|
||||
import { Essentials } from './Essentials';
|
||||
|
||||
function mockSectionsDto(): SectionsDto {
|
||||
return {
|
||||
sections: [
|
||||
{
|
||||
title: 'Detect',
|
||||
description: 'Configure something1',
|
||||
steps: [
|
||||
{
|
||||
title: 'Create something1',
|
||||
description: 'description1',
|
||||
button: {
|
||||
type: 'openLink',
|
||||
urlLink: {
|
||||
url: '/url1',
|
||||
},
|
||||
label: 'label1',
|
||||
},
|
||||
done: true,
|
||||
},
|
||||
{
|
||||
title: 'Create something2',
|
||||
description: 'description2',
|
||||
button: {
|
||||
type: 'openLink',
|
||||
urlLink: {
|
||||
url: '/url2',
|
||||
},
|
||||
label: 'label2',
|
||||
},
|
||||
done: false,
|
||||
},
|
||||
{
|
||||
title: 'testing step',
|
||||
description: 'description3',
|
||||
button: {
|
||||
type: 'openLink',
|
||||
urlLink: {
|
||||
url: '/url3',
|
||||
},
|
||||
label: 'label3',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'testing step',
|
||||
description: 'description3',
|
||||
button: {
|
||||
type: 'openLink',
|
||||
urlLink: {
|
||||
url: '/url4',
|
||||
},
|
||||
urlLinkOnDone: {
|
||||
url: '/url4',
|
||||
},
|
||||
label: 'testNotDone',
|
||||
labelOnDone: 'testDone',
|
||||
},
|
||||
done: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
describe('Essentials', () => {
|
||||
it('renders progress status correctly', async () => {
|
||||
const essentialsConfig = mockSectionsDto();
|
||||
const stepsDone = 2;
|
||||
const totalStepsToDo = 5;
|
||||
render(
|
||||
<Essentials
|
||||
essentialsConfig={essentialsConfig}
|
||||
stepsDone={stepsDone}
|
||||
totalStepsToDo={totalStepsToDo}
|
||||
onClose={jest.fn()}
|
||||
/>
|
||||
);
|
||||
const progressBar = screen.getByText(/your progress/i);
|
||||
expect(within(progressBar).getByText(/2/i)).toBeInTheDocument();
|
||||
expect(within(progressBar).getByText(/of 5/i)).toBeInTheDocument();
|
||||
});
|
||||
it('renders steps correctly', async () => {
|
||||
const essentialsConfig = mockSectionsDto();
|
||||
const stepsDone = 0;
|
||||
const totalStepsToDo = 5;
|
||||
render(
|
||||
<Essentials
|
||||
essentialsConfig={essentialsConfig}
|
||||
stepsDone={stepsDone}
|
||||
totalStepsToDo={totalStepsToDo}
|
||||
onClose={jest.fn()}
|
||||
/>
|
||||
);
|
||||
// step1 is done and labelonDone is not defined
|
||||
expect(byRole('heading', { name: /detect/i }).get()).toBeInTheDocument();
|
||||
const step1 = screen.getAllByTestId('step')[0];
|
||||
expect(within(step1).getByText(/create something1/i)).toBeInTheDocument();
|
||||
expect(within(step1).getByTestId('checked-step')).toBeInTheDocument();
|
||||
expect(within(step1).queryByRole('link', { name: /label1/i })).not.toBeInTheDocument();
|
||||
// step2 is not done
|
||||
const step2 = screen.getAllByTestId('step')[1];
|
||||
expect(within(step2).getByText(/create something2/i)).toBeInTheDocument();
|
||||
expect(within(step2).getByTestId('unckecked-step')).toBeInTheDocument();
|
||||
expect(byRole('link', { name: /label2/i }).get()).toBeInTheDocument();
|
||||
|
||||
// step3 , done is not defined
|
||||
const step3 = screen.getAllByTestId('step')[2];
|
||||
expect(within(step3).getByText(/testing step/i)).toBeInTheDocument();
|
||||
expect(within(step3).queryByTestId('checked-step')).not.toBeInTheDocument();
|
||||
expect(within(step3).queryByTestId('unckecked-step')).not.toBeInTheDocument();
|
||||
expect(within(step3).queryByTestId('step-button')).not.toBeInTheDocument();
|
||||
expect(within(step3).queryByRole('link', { name: /label3/i })).toBeInTheDocument();
|
||||
|
||||
// step4 is done and labelonDone is defined
|
||||
const step4 = screen.getAllByTestId('step')[3];
|
||||
expect(within(step4).getByText(/testing step/i)).toBeInTheDocument();
|
||||
expect(within(step4).getByTestId('checked-step')).toBeInTheDocument();
|
||||
expect(within(step4).queryByRole('link', { name: /testNotDone/i })).not.toBeInTheDocument();
|
||||
expect(within(step4).queryByRole('link', { name: /testDone/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1,195 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, Drawer, Dropdown, Icon, LinkButton, Menu, Stack, Text, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { createUrl } from 'app/features/alerting/unified/utils/url';
|
||||
|
||||
import { SectionDto, SectionDtoStep, SectionsDto, StepButtonDto } from '../irmHooks';
|
||||
|
||||
import { ProgressBar, StepsStatus } from './ProgressBar';
|
||||
|
||||
export interface EssentialsProps {
|
||||
onClose: () => void;
|
||||
essentialsConfig: SectionsDto;
|
||||
stepsDone: number;
|
||||
totalStepsToDo: number;
|
||||
}
|
||||
|
||||
export function Essentials({ onClose, essentialsConfig, stepsDone, totalStepsToDo }: EssentialsProps) {
|
||||
return (
|
||||
<Drawer title="Essentials" subtitle="Complete the following configuration tasks" onClose={onClose}>
|
||||
<EssentialContent essentialContent={essentialsConfig} stepsDone={stepsDone} totalStepsToDo={totalStepsToDo} />
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export function EssentialContent({
|
||||
essentialContent,
|
||||
stepsDone,
|
||||
totalStepsToDo,
|
||||
}: {
|
||||
essentialContent: SectionsDto;
|
||||
stepsDone: number;
|
||||
totalStepsToDo: number;
|
||||
}) {
|
||||
return (
|
||||
<Stack direction={'column'} gap={1}>
|
||||
<ProgressStatus stepsDone={stepsDone} totalStepsToDo={totalStepsToDo} />
|
||||
{essentialContent.sections.map((section: SectionDto) => (
|
||||
<Section key={section.title} section={section} />
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
interface SectionProps {
|
||||
section: SectionDto;
|
||||
}
|
||||
function Section({ section }: SectionProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Text element="h4">{section.title}</Text>
|
||||
|
||||
<Text color="secondary">{section.description}</Text>
|
||||
<Stack direction={'column'} gap={2}>
|
||||
{section.steps.map((step, index) => (
|
||||
<Step key={index} step={step} />
|
||||
))}
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function DoneIcon({ done }: { done: boolean }) {
|
||||
return done ? (
|
||||
<Icon name="check-circle" color="green" data-testid="checked-step" />
|
||||
) : (
|
||||
<Icon name="circle" data-testid="unckecked-step" />
|
||||
);
|
||||
}
|
||||
interface StepProps {
|
||||
step: SectionDtoStep;
|
||||
}
|
||||
|
||||
function Step({ step }: StepProps) {
|
||||
return (
|
||||
<Stack direction={'row'} justifyContent={'space-between'} data-testid="step">
|
||||
<Stack direction={'row'} alignItems="center">
|
||||
{step.done !== undefined && <DoneIcon done={step.done} />}
|
||||
<Text variant="body">{step.title}</Text>
|
||||
<Tooltip content={step.description} placement="right">
|
||||
<Icon name="question-circle" />
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
<StepButton {...step.button} done={step.done} data-testid="step-button" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
interface LinkButtonProps {
|
||||
urlLink?: { url: string; queryParams?: Record<string, string> };
|
||||
label: string;
|
||||
urlLinkOnDone?: { url: string; queryParams?: Record<string, string> };
|
||||
labelOnDone?: string;
|
||||
done?: boolean;
|
||||
}
|
||||
function OpenLinkButton(props: LinkButtonProps) {
|
||||
const { urlLink, label, urlLinkOnDone, labelOnDone, done } = props;
|
||||
const urlToGoWhenNotDone = urlLink?.url
|
||||
? createUrl(urlLink.url, {
|
||||
returnTo: location.pathname + location.search,
|
||||
...urlLink.queryParams,
|
||||
})
|
||||
: '';
|
||||
const urlToGoWhenDone = urlLinkOnDone?.url
|
||||
? createUrl(urlLinkOnDone.url, {
|
||||
returnTo: location.pathname + location.search,
|
||||
...urlLinkOnDone.queryParams,
|
||||
})
|
||||
: '';
|
||||
const urlToGo = done ? urlToGoWhenDone : urlToGoWhenNotDone;
|
||||
return (
|
||||
<LinkButton href={urlToGo} variant="secondary">
|
||||
{done ? labelOnDone ?? label : label}
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
|
||||
interface StepButtonProps extends StepButtonDto {
|
||||
done?: boolean;
|
||||
}
|
||||
function StepButton({
|
||||
type,
|
||||
urlLink,
|
||||
urlLinkOnDone,
|
||||
label,
|
||||
labelOnDone,
|
||||
options,
|
||||
onClickOption,
|
||||
done,
|
||||
stepNotAvailableText,
|
||||
}: StepButtonProps) {
|
||||
switch (type) {
|
||||
case 'openLink':
|
||||
return (
|
||||
<OpenLinkButton
|
||||
urlLink={urlLink}
|
||||
label={label}
|
||||
urlLinkOnDone={urlLinkOnDone}
|
||||
labelOnDone={labelOnDone}
|
||||
done={done}
|
||||
/>
|
||||
);
|
||||
case 'dropDown':
|
||||
if (Boolean(options?.length)) {
|
||||
return (
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
{options?.map((option) => (
|
||||
<Menu.Item
|
||||
label={option.label}
|
||||
key={option.value}
|
||||
onClick={() => {
|
||||
onClickOption?.(option.value);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button variant="secondary" size="md">
|
||||
{label}
|
||||
<Icon name="angle-down" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
} else {
|
||||
return <Text>{stepNotAvailableText ?? 'No options available'} </Text>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ProgressStatus({ stepsDone, totalStepsToDo }: { stepsDone: number; totalStepsToDo: number }) {
|
||||
return (
|
||||
<Stack direction={'row'} gap={1} alignItems="center">
|
||||
Your progress
|
||||
<ProgressBar stepsDone={stepsDone} totalStepsToDo={totalStepsToDo} />
|
||||
<StepsStatus stepsDone={stepsDone} totalStepsToDo={totalStepsToDo} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
wrapper: css({
|
||||
margin: theme.spacing(2, 0),
|
||||
padding: theme.spacing(2),
|
||||
border: `1px solid ${theme.colors.border.medium}`,
|
||||
borderRadius: theme.shape.radius.default,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
}),
|
||||
};
|
||||
};
|
@ -0,0 +1,43 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Text, useStyles2 } from '@grafana/ui';
|
||||
|
||||
export function ProgressBar({ stepsDone, totalStepsToDo }: { stepsDone: number; totalStepsToDo: number }) {
|
||||
const styles = useStyles2(getStyles);
|
||||
if (totalStepsToDo === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={styles.containerStyles} role="progressbar" aria-valuenow={stepsDone} aria-valuemax={totalStepsToDo}>
|
||||
<div className={styles.fillerStyles((stepsDone / totalStepsToDo) * 100)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export function StepsStatus({ stepsDone, totalStepsToDo }: { stepsDone: number; totalStepsToDo: number }) {
|
||||
return (
|
||||
<div>
|
||||
<Text color="success">{stepsDone}</Text> of {totalStepsToDo}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
containerStyles: css({
|
||||
height: theme.spacing(2),
|
||||
borderRadius: theme.shape.radius.pill,
|
||||
backgroundColor: theme.colors.border.weak,
|
||||
flex: 'auto',
|
||||
}),
|
||||
fillerStyles: (stepsDone: number) =>
|
||||
css({
|
||||
height: '100%',
|
||||
width: `${stepsDone}%`,
|
||||
backgroundColor: theme.colors.success.main,
|
||||
borderRadius: theme.shape.radius.pill,
|
||||
textAlign: 'right',
|
||||
}),
|
||||
};
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { incidentsApi } from 'app/features/alerting/unified/api/incidentsApi';
|
||||
import { usePluginBridge } from 'app/features/alerting/unified/hooks/usePluginBridge';
|
||||
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
|
||||
|
||||
interface IncidentsPluginConfig {
|
||||
isInstalled: boolean;
|
||||
isChatOpsInstalled: boolean;
|
||||
isIncidentCreated: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function useGetIncidentPluginConfig(): IncidentsPluginConfig {
|
||||
const { installed: incidentPluginInstalled, loading: loadingPluginSettings } = usePluginBridge(
|
||||
SupportedPlugin.Incident
|
||||
);
|
||||
const { data: incidentsConfig, isLoading: loadingPluginConfig } =
|
||||
incidentsApi.endpoints.getIncidentsPluginConfig.useQuery();
|
||||
|
||||
return {
|
||||
isInstalled: incidentPluginInstalled ?? false,
|
||||
isChatOpsInstalled: incidentsConfig?.isChatOpsInstalled ?? false,
|
||||
isIncidentCreated: incidentsConfig?.isIncidentCreated ?? false,
|
||||
isLoading: loadingPluginSettings || loadingPluginConfig,
|
||||
};
|
||||
}
|
322
public/app/features/gops/configuration-tracker/irmHooks.ts
Normal file
322
public/app/features/gops/configuration-tracker/irmHooks.ts
Normal file
@ -0,0 +1,322 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { createUrl } from 'app/features/alerting/unified/utils/url';
|
||||
|
||||
import {
|
||||
isOnCallContactPointReady,
|
||||
useGetContactPoints,
|
||||
useGetDefaultContactPoint,
|
||||
useIsCreateAlertRuleDone,
|
||||
} from './alerting/hooks';
|
||||
import { isContactPointReady } from './alerting/utils';
|
||||
import { ConfigurationStepsEnum, DataSourceConfigurationData, IrmCardConfiguration } from './components/ConfigureIRM';
|
||||
import { useGetIncidentPluginConfig } from './incidents/hooks';
|
||||
import { useOnCallChatOpsConnections, useOnCallOptions } from './onCall/hooks';
|
||||
|
||||
interface UrlLink {
|
||||
url: string;
|
||||
queryParams?: Record<string, string>;
|
||||
}
|
||||
export interface StepButtonDto {
|
||||
type: 'openLink' | 'dropDown';
|
||||
label: string;
|
||||
labelOnDone?: string;
|
||||
urlLink?: UrlLink; // only for openLink
|
||||
urlLinkOnDone?: UrlLink; // only for openLink
|
||||
options?: Array<{ label: string; value: string }>; // only for dropDown
|
||||
onClickOption?: (value: string) => void; // only for dropDown
|
||||
stepNotAvailableText?: string;
|
||||
}
|
||||
export interface SectionDtoStep {
|
||||
title: string;
|
||||
description: string;
|
||||
button: StepButtonDto;
|
||||
done?: boolean;
|
||||
}
|
||||
export interface SectionDto {
|
||||
title: string;
|
||||
description: string;
|
||||
steps: SectionDtoStep[];
|
||||
}
|
||||
export interface SectionsDto {
|
||||
sections: SectionDto[];
|
||||
}
|
||||
|
||||
export interface EssentialsConfigurationData {
|
||||
essentialContent: SectionsDto;
|
||||
stepsDone: number;
|
||||
totalStepsToDo: number;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
function useGetConfigurationForApps() {
|
||||
// configuration checks for alerting
|
||||
const { contactPoints, isLoading: isLoadingContactPoints } = useGetContactPoints();
|
||||
const { defaultContactpoint, isLoading: isLoadingDefaultContactPoint } = useGetDefaultContactPoint();
|
||||
const { isDone: isCreateAlertRuleDone, isLoading: isLoadingAlertCreatedDone } = useIsCreateAlertRuleDone();
|
||||
// configuration checks for incidents
|
||||
const {
|
||||
isChatOpsInstalled,
|
||||
isInstalled: isIncidentsInstalled,
|
||||
isLoading: isIncidentsConfigLoading,
|
||||
} = useGetIncidentPluginConfig();
|
||||
// configuration checks for oncall
|
||||
const onCallOptions = useOnCallOptions();
|
||||
const {
|
||||
is_chatops_connected,
|
||||
is_integration_chatops_connected,
|
||||
isLoading: isOnCallConfigLoading,
|
||||
} = useOnCallChatOpsConnections();
|
||||
// check if any of the configurations are loading
|
||||
const isLoading =
|
||||
isLoadingContactPoints ||
|
||||
isLoadingDefaultContactPoint ||
|
||||
isLoadingAlertCreatedDone ||
|
||||
isIncidentsConfigLoading ||
|
||||
isOnCallConfigLoading;
|
||||
|
||||
return {
|
||||
alerting: {
|
||||
contactPoints,
|
||||
defaultContactpoint,
|
||||
isCreateAlertRuleDone,
|
||||
},
|
||||
incidents: {
|
||||
isChatOpsInstalled,
|
||||
isIncidentsInstalled,
|
||||
},
|
||||
onCall: {
|
||||
onCallOptions,
|
||||
is_chatops_connected,
|
||||
is_integration_chatops_connected,
|
||||
},
|
||||
isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
export function useGetEssentialsConfiguration(): EssentialsConfigurationData {
|
||||
const {
|
||||
alerting: { contactPoints, defaultContactpoint, isCreateAlertRuleDone },
|
||||
incidents: { isChatOpsInstalled, isIncidentsInstalled },
|
||||
onCall: { onCallOptions, is_chatops_connected, is_integration_chatops_connected },
|
||||
isLoading,
|
||||
} = useGetConfigurationForApps();
|
||||
|
||||
function onIntegrationClick(integrationId: string, url: string) {
|
||||
const urlToGoWithIntegration = createUrl(url + integrationId, {
|
||||
returnTo: location.pathname + location.search,
|
||||
});
|
||||
locationService.push(urlToGoWithIntegration);
|
||||
}
|
||||
|
||||
const essentialContent: SectionsDto = {
|
||||
sections: [
|
||||
{
|
||||
title: 'Detect',
|
||||
description: 'Configure Grafana Alerting',
|
||||
steps: [
|
||||
{
|
||||
title: 'Update default email contact point',
|
||||
description: 'Add a valid email to the default email contact point.',
|
||||
button: {
|
||||
type: 'openLink',
|
||||
urlLink: {
|
||||
url: `/alerting/notifications/receivers/${defaultContactpoint}/edit`,
|
||||
queryParams: { alertmanager: 'grafana' },
|
||||
},
|
||||
label: 'Edit',
|
||||
labelOnDone: 'View',
|
||||
urlLinkOnDone: {
|
||||
url: `/alerting/notifications`,
|
||||
},
|
||||
},
|
||||
done: isContactPointReady(defaultContactpoint, contactPoints),
|
||||
},
|
||||
{
|
||||
title: 'Connect alerting to OnCall',
|
||||
description: 'Create an OnCall integration for an alerting contact point.',
|
||||
button: {
|
||||
type: 'openLink',
|
||||
urlLink: {
|
||||
url: '/alerting/notifications/receivers/new',
|
||||
},
|
||||
label: 'Connect',
|
||||
urlLinkOnDone: {
|
||||
url: '/alerting/notifications',
|
||||
},
|
||||
labelOnDone: 'View',
|
||||
},
|
||||
done: isOnCallContactPointReady(contactPoints),
|
||||
},
|
||||
{
|
||||
title: 'Create alert rule',
|
||||
description: 'Create an alert rule to monitor your system.',
|
||||
button: {
|
||||
type: 'openLink',
|
||||
urlLink: {
|
||||
url: '/alerting/new',
|
||||
},
|
||||
label: 'Create',
|
||||
urlLinkOnDone: {
|
||||
url: '/alerting/list',
|
||||
},
|
||||
labelOnDone: 'View',
|
||||
},
|
||||
done: isCreateAlertRuleDone,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Respond',
|
||||
description: 'Configure OnCall and Incident',
|
||||
steps: [
|
||||
{
|
||||
title: 'Initialize Incident plugin',
|
||||
description: 'Initialize the Incident plugin to declare and manage incidents.',
|
||||
button: {
|
||||
type: 'openLink',
|
||||
urlLink: {
|
||||
url: '/a/grafana-incident-app/walkthrough/generate-key',
|
||||
},
|
||||
label: 'Initialize',
|
||||
urlLinkOnDone: {
|
||||
url: '/a/grafana-incident-app',
|
||||
},
|
||||
labelOnDone: 'View',
|
||||
},
|
||||
done: isIncidentsInstalled,
|
||||
},
|
||||
{
|
||||
title: 'Connect your Messaging workspace to OnCall',
|
||||
description: 'Receive alerts and oncall notifications within your chat environment.',
|
||||
button: {
|
||||
type: 'openLink',
|
||||
urlLink: {
|
||||
url: '/a/grafana-oncall-app/settings',
|
||||
queryParams: { tab: 'ChatOps', chatOpsTab: 'Slack' },
|
||||
},
|
||||
label: 'Connect',
|
||||
urlLinkOnDone: {
|
||||
url: '/a/grafana-oncall-app/settings',
|
||||
queryParams: { tab: 'ChatOps' },
|
||||
},
|
||||
labelOnDone: 'View',
|
||||
},
|
||||
done: is_chatops_connected,
|
||||
},
|
||||
{
|
||||
title: 'Connect your Messaging workspace to Incident',
|
||||
description:
|
||||
'Automatically create an incident channel and manage incidents directly within your chat environment.',
|
||||
button: {
|
||||
type: 'openLink',
|
||||
urlLink: {
|
||||
url: '/a/grafana-incident-app/integrations/grate.slack',
|
||||
},
|
||||
label: 'Connect',
|
||||
urlLinkOnDone: {
|
||||
url: '/a/grafana-incident-app/integrations',
|
||||
},
|
||||
},
|
||||
done: isChatOpsInstalled,
|
||||
},
|
||||
{
|
||||
title: 'Add Messaging workspace channel to OnCall Integration',
|
||||
description: 'Select ChatOps channels to route notifications',
|
||||
button: {
|
||||
type: 'openLink',
|
||||
urlLink: {
|
||||
url: '/a/grafana-oncall-app/integrations/',
|
||||
},
|
||||
label: 'Add',
|
||||
urlLinkOnDone: {
|
||||
url: '/a/grafana-oncall-app/integrations/',
|
||||
},
|
||||
labelOnDone: 'View',
|
||||
},
|
||||
done: is_integration_chatops_connected,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Test your configuration',
|
||||
description: '',
|
||||
steps: [
|
||||
{
|
||||
title: 'Send OnCall demo alert',
|
||||
description: 'In the integration page, click Send demo alert, to review your notification',
|
||||
button: {
|
||||
type: 'dropDown',
|
||||
label: 'Select integration',
|
||||
options: onCallOptions,
|
||||
onClickOption: (value) => onIntegrationClick(value, '/a/grafana-oncall-app/integrations/'),
|
||||
stepNotAvailableText: 'No integrations available',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Create Incident drill',
|
||||
description: 'Practice solving an Incident',
|
||||
button: {
|
||||
type: 'openLink',
|
||||
urlLink: {
|
||||
url: '/a/grafana-incident-app',
|
||||
queryParams: { declare: 'new', drill: '1' },
|
||||
},
|
||||
label: 'Start drill',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const { stepsDone, totalStepsToDo } = essentialContent.sections.reduce(
|
||||
(acc, section) => {
|
||||
const stepsDone = section.steps.filter((step) => step.done).length;
|
||||
const totalStepsToForSection = section.steps.reduce((acc, step) => (step.done !== undefined ? acc + 1 : acc), 0);
|
||||
return {
|
||||
stepsDone: acc.stepsDone + stepsDone,
|
||||
totalStepsToDo: acc.totalStepsToDo + totalStepsToForSection,
|
||||
};
|
||||
},
|
||||
{ stepsDone: 0, totalStepsToDo: 0 }
|
||||
);
|
||||
return { essentialContent, stepsDone, totalStepsToDo, isLoading };
|
||||
}
|
||||
interface UseConfigurationProps {
|
||||
dataSourceConfigurationData: DataSourceConfigurationData;
|
||||
essentialsConfigurationData: EssentialsConfigurationData;
|
||||
}
|
||||
|
||||
export const useGetConfigurationForUI = ({
|
||||
dataSourceConfigurationData: { dataSourceCompatibleWithAlerting },
|
||||
essentialsConfigurationData: { stepsDone, totalStepsToDo },
|
||||
}: UseConfigurationProps): IrmCardConfiguration[] => {
|
||||
return useMemo(() => {
|
||||
function getConnectDataSourceConfiguration() {
|
||||
const description = dataSourceCompatibleWithAlerting
|
||||
? 'You have connected a datasource.'
|
||||
: 'Connect at least one data source to start receiving data.';
|
||||
const actionButtonTitle = dataSourceCompatibleWithAlerting ? 'View' : 'Connect';
|
||||
return {
|
||||
id: ConfigurationStepsEnum.CONNECT_DATASOURCE,
|
||||
title: 'Connect data source',
|
||||
description,
|
||||
actionButtonTitle,
|
||||
isDone: dataSourceCompatibleWithAlerting,
|
||||
};
|
||||
}
|
||||
return [
|
||||
getConnectDataSourceConfiguration(),
|
||||
{
|
||||
id: ConfigurationStepsEnum.ESSENTIALS,
|
||||
title: 'Essentials',
|
||||
titleIcon: 'star',
|
||||
description: 'Configure the features you need to start using Grafana IRM workflows',
|
||||
actionButtonTitle: 'Start',
|
||||
stepsDone,
|
||||
totalStepsToDo,
|
||||
},
|
||||
];
|
||||
}, [dataSourceCompatibleWithAlerting, stepsDone, totalStepsToDo]);
|
||||
};
|
@ -0,0 +1,45 @@
|
||||
import { onCallApi } from 'app/features/alerting/unified/api/onCallApi';
|
||||
import { usePluginBridge } from 'app/features/alerting/unified/hooks/usePluginBridge';
|
||||
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
|
||||
|
||||
export function useGetOnCallIntegrations() {
|
||||
const { installed: onCallPluginInstalled } = usePluginBridge(SupportedPlugin.OnCall);
|
||||
|
||||
const { data: onCallIntegrations } = onCallApi.endpoints.grafanaOnCallIntegrations.useQuery(undefined, {
|
||||
skip: !onCallPluginInstalled,
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
refetchOnMountOrArgChange: true,
|
||||
});
|
||||
|
||||
return onCallIntegrations ?? [];
|
||||
}
|
||||
|
||||
function useGetOnCallConfigurationChecks() {
|
||||
const { data: onCallConfigChecks, isLoading } = onCallApi.endpoints.onCallConfigChecks.useQuery(undefined, {
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
refetchOnMountOrArgChange: true,
|
||||
});
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
onCallConfigChecks: onCallConfigChecks ?? { is_chatops_connected: false, is_integration_chatops_connected: false },
|
||||
};
|
||||
}
|
||||
|
||||
export function useOnCallOptions() {
|
||||
const onCallIntegrations = useGetOnCallIntegrations();
|
||||
return onCallIntegrations.map((integration) => ({
|
||||
label: integration.display_name,
|
||||
value: integration.value,
|
||||
}));
|
||||
}
|
||||
|
||||
export function useOnCallChatOpsConnections() {
|
||||
const {
|
||||
onCallConfigChecks: { is_chatops_connected, is_integration_chatops_connected },
|
||||
isLoading,
|
||||
} = useGetOnCallConfigurationChecks();
|
||||
return { is_chatops_connected, is_integration_chatops_connected, isLoading };
|
||||
}
|
@ -10,10 +10,12 @@ import { contextSrv } from 'app/core/services/context_srv';
|
||||
import UserAdminPage from 'app/features/admin/UserAdminPage';
|
||||
import LdapPage from 'app/features/admin/ldap/LdapPage';
|
||||
import { getAlertingRoutes } from 'app/features/alerting/routes';
|
||||
import { isAdmin, isLocalDevEnv, isOpenSourceEdition } from 'app/features/alerting/unified/utils/misc';
|
||||
import { ConnectionsRedirectNotice } from 'app/features/connections/components/ConnectionsRedirectNotice';
|
||||
import { ROUTES as CONNECTIONS_ROUTES } from 'app/features/connections/constants';
|
||||
import { getRoutes as getDataConnectionsRoutes } from 'app/features/connections/routes';
|
||||
import { DATASOURCES_ROUTES } from 'app/features/datasources/constants';
|
||||
import { ConfigureIRM } from 'app/features/gops/configuration-tracker/components/ConfigureIRM';
|
||||
import { getRoutes as getPluginCatalogRoutes } from 'app/features/plugins/admin/routes';
|
||||
import { getAppPluginRoutes } from 'app/features/plugins/routes';
|
||||
import { getProfileRoutes } from 'app/features/profile/routes';
|
||||
@ -171,7 +173,14 @@ export function getAppRoutes(): RouteDescriptor[] {
|
||||
},
|
||||
{
|
||||
path: '/alerts-and-incidents',
|
||||
component: () => <NavLandingPage navId="alerts-and-incidents" />,
|
||||
component: () => {
|
||||
return (
|
||||
<NavLandingPage
|
||||
navId="alerts-and-incidents"
|
||||
header={(!isOpenSourceEdition() && isAdmin()) || isLocalDevEnv() ? <ConfigureIRM /> : undefined}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/testing-and-synthetics',
|
||||
|
Loading…
Reference in New Issue
Block a user