From c68d7f1e35eadb92d5b37acb46d7cf3f35c86119 Mon Sep 17 00:00:00 2001 From: Giordano Ricci Date: Fri, 26 Aug 2022 11:27:28 +0100 Subject: [PATCH] Correlations: Add CorrelationSettings Page (#53821) * GrafanaUI: add option to close DeleteButton on confirm click * add datasource readOnly info to frontend settings * move isTruthy utility type guard * add generic non-visualization table component * Add correlations settings page * add missing readOnly in mock * Fix typo * avoid reloading correlations after add/remove * use DeepPartial from rhf * validate source data source * fix validation logic * fix navmodel test * add missing readonly property * remove unused styles * handle multiple clicks on elements * better UX for loading states * fix remove handler * add glue icon --- packages/grafana-data/src/types/datasource.ts | 1 + .../components/ConfirmButton/DeleteButton.tsx | 5 +- packages/grafana-ui/src/types/icon.ts | 1 + pkg/api/api.go | 4 +- pkg/api/frontendsettings.go | 1 + pkg/api/index.go | 11 + pkg/plugins/models.go | 1 + pkg/services/correlations/accesscontrol.go | 11 + .../src/InputDatasource.test.ts | 1 + .../NavBar/navBarItem-translations.ts | 1 + public/app/core/reducers/navModel.test.ts | 3 + public/app/core/reducers/navModel.ts | 1 + public/app/core/utils/types.ts | 3 + .../unified/RedirectToRuleViewer.test.tsx | 3 + .../alerting/unified/RuleViewer.test.tsx | 1 + public/app/features/alerting/unified/mocks.ts | 1 + .../alerting/unified/utils/query.test.ts | 1 + .../correlations/CorrelationsPage.test.tsx | 412 ++++++++++++++++++ .../correlations/CorrelationsPage.tsx | 215 +++++++++ .../correlations/Forms/AddCorrelationForm.tsx | 123 ++++++ .../Forms/CorrelationDetailsFormPart.tsx | 59 +++ .../Forms/EditCorrelationForm.tsx | 50 +++ .../app/features/correlations/Forms/types.ts | 11 + .../correlations/Forms/useCorrelationForm.ts | 18 + .../components/EmptyCorrelationsCTA.tsx | 20 + .../components/Table/ExpanderCell.tsx | 22 + .../correlations/components/Table/index.tsx | 161 +++++++ .../correlations/components/Table/utils.ts | 36 ++ public/app/features/correlations/types.ts | 17 + .../features/correlations/useCorrelations.ts | 88 ++++ .../services/PublicDashboardDataSource.ts | 1 + .../features/explore/spec/helper/setup.tsx | 1 + .../expressions/ExpressionDatasource.ts | 1 + .../variables/shared/testing/helpers.ts | 1 + .../cloudMonitoringInstanceSettings.ts | 1 + .../cloud-monitoring/specs/testData.ts | 1 + .../configuration/ElasticDetails.tsx | 3 +- .../elasticsearch/configuration/utils.ts | 4 - .../elasticsearch/datasource.test.ts | 1 + .../__mocks__/instanceSettings.ts | 1 + .../jaeger/components/SearchForm.test.tsx | 1 + .../datasource/jaeger/datasource.test.ts | 1 + .../LokiQueryBuilderContainer.test.tsx | 1 + .../LokiQueryEditorSelector.test.tsx | 1 + .../components/UnwrapParamEditor.test.tsx | 1 + .../PromQueryEditorSelector.test.tsx | 1 + .../datasource/tempo/datasource.test.ts | 1 + .../tempo/resultTransformer.test.ts | 1 + .../datasource/zipkin/datasource.test.ts | 1 + public/app/routes/routes.tsx | 6 + public/img/icons/custom/gf-glue.svg | 3 + 51 files changed, 1307 insertions(+), 8 deletions(-) create mode 100644 pkg/services/correlations/accesscontrol.go create mode 100644 public/app/core/utils/types.ts create mode 100644 public/app/features/correlations/CorrelationsPage.test.tsx create mode 100644 public/app/features/correlations/CorrelationsPage.tsx create mode 100644 public/app/features/correlations/Forms/AddCorrelationForm.tsx create mode 100644 public/app/features/correlations/Forms/CorrelationDetailsFormPart.tsx create mode 100644 public/app/features/correlations/Forms/EditCorrelationForm.tsx create mode 100644 public/app/features/correlations/Forms/types.ts create mode 100644 public/app/features/correlations/Forms/useCorrelationForm.ts create mode 100644 public/app/features/correlations/components/EmptyCorrelationsCTA.tsx create mode 100644 public/app/features/correlations/components/Table/ExpanderCell.tsx create mode 100644 public/app/features/correlations/components/Table/index.tsx create mode 100644 public/app/features/correlations/components/Table/utils.ts create mode 100644 public/app/features/correlations/types.ts create mode 100644 public/app/features/correlations/useCorrelations.ts create mode 100644 public/img/icons/custom/gf-glue.svg diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index 94399d04730..3ecc60709e3 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -574,6 +574,7 @@ export interface DataSourceInstanceSettings = ({ size, disabled, onConfirm, 'aria-label': ariaLabel }) => { +export const DeleteButton: FC = ({ size, disabled, onConfirm, 'aria-label': ariaLabel, closeOnConfirm }) => { return ( = ({ size, disabled, onConfirm, 'aria-label size={size || 'md'} disabled={disabled} onConfirm={onConfirm} + closeOnConfirm={closeOnConfirm} > + )} + + + + {!data && get.loading && ( +
+ +
+ )} + + {showEmptyListCTA && setIsAdding(true)} />} + + { + // This error is not actionable, it'd be nice to have a recovery button + get.error && get.error.status !== 404 && ( + + + {get.error.data.message || + 'An unknown error occurred while fetching correlation data. Please try again.'} + + + ) + } + + {isAdding && setIsAdding(false)} onCreated={handleAdd} />} + + {data && data.length >= 1 && ( + ( + + )} + columns={columns} + data={data} + getRowId={(correlation) => `${correlation.source.uid}-${correlation.uid}`} + /> + )} + + + ); +} + +const getDatasourceCellStyles = (theme: GrafanaTheme2) => ({ + root: css` + display: flex; + align-items: center; + `, + dsLogo: css` + margin-right: ${theme.spacing()}; + height: 16px; + width: 16px; + `, +}); + +const DataSourceCell = memo( + function DataSourceCell({ + cell: { value }, + }: CellProps) { + const styles = useStyles2(getDatasourceCellStyles); + + return ( + + + {value.name} + + ); + }, + ({ cell: { value } }, { cell: { value: prevValue } }) => { + return value.type === prevValue.type && value.name === prevValue.name; + } +); + +const noWrap = css` + white-space: nowrap; +`; + +const InfoCell = memo( + function InfoCell({ ...props }: CellProps) { + const readOnly = props.row.original.source.readOnly; + + if (readOnly) { + return ; + } else { + return null; + } + }, + (props, prevProps) => props.row.original.source.readOnly === prevProps.row.original.source.readOnly +); diff --git a/public/app/features/correlations/Forms/AddCorrelationForm.tsx b/public/app/features/correlations/Forms/AddCorrelationForm.tsx new file mode 100644 index 00000000000..21460a4d1bd --- /dev/null +++ b/public/app/features/correlations/Forms/AddCorrelationForm.tsx @@ -0,0 +1,123 @@ +import { css } from '@emotion/css'; +import React, { useCallback } from 'react'; +import { Controller } from 'react-hook-form'; + +import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data'; +import { DataSourcePicker } from '@grafana/runtime'; +import { Button, Field, HorizontalGroup, PanelContainer, useStyles2 } from '@grafana/ui'; +import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; +import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; + +import { useCorrelations } from '../useCorrelations'; + +import { CorrelationDetailsFormPart } from './CorrelationDetailsFormPart'; +import { FormDTO } from './types'; +import { useCorrelationForm } from './useCorrelationForm'; + +const getStyles = (theme: GrafanaTheme2) => ({ + panelContainer: css` + position: relative; + padding: ${theme.spacing(1)}; + margin-bottom: ${theme.spacing(2)}; + `, + linksToContainer: css` + flex-grow: 1; + /* This is the width of the textarea minus the sum of the label&description fields, + * so that this element takes exactly the remaining space and the inputs will be + * nicely aligned with the textarea + **/ + max-width: ${theme.spacing(80 - 64)}; + margin-top: ${theme.spacing(3)}; + text-align: right; + padding-right: ${theme.spacing(1)}; + `, + // we can't use HorizontalGroup because it wraps elements in divs and sets margins on them + horizontalGroup: css` + display: flex; + `, +}); + +interface Props { + onClose: () => void; + onCreated: () => void; +} + +const withDsUID = (fn: Function) => (ds: DataSourceInstanceSettings) => fn(ds.uid); + +export const AddCorrelationForm = ({ onClose, onCreated }: Props) => { + const styles = useStyles2(getStyles); + + const { create } = useCorrelations(); + + const onSubmit = useCallback( + async (correlation) => { + await create.execute(correlation); + onCreated(); + }, + [create, onCreated] + ); + + const { control, handleSubmit, register, errors } = useCorrelationForm({ onSubmit }); + + return ( + + +
+
+ + !getDatasourceSrv().getInstanceSettings(uid)?.readOnly || "Source can't be a read-only data source.", + }, + }} + render={({ field: { onChange, value } }) => ( + + + + )} + /> +
Links to
+ ( + + + + )} + /> +
+ + + + + + + +
+ ); +}; diff --git a/public/app/features/correlations/Forms/CorrelationDetailsFormPart.tsx b/public/app/features/correlations/Forms/CorrelationDetailsFormPart.tsx new file mode 100644 index 00000000000..dccb18a4148 --- /dev/null +++ b/public/app/features/correlations/Forms/CorrelationDetailsFormPart.tsx @@ -0,0 +1,59 @@ +import { css, cx } from '@emotion/css'; +import React from 'react'; +import { RegisterOptions, UseFormRegisterReturn } from 'react-hook-form'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Field, Input, TextArea, useStyles2 } from '@grafana/ui'; + +import { EditFormDTO } from './types'; + +const getInputId = (inputName: string, correlation?: EditFormDTO) => { + if (!correlation) { + return inputName; + } + + return `${inputName}_${correlation.sourceUID}-${correlation.uid}`; +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + marginless: css` + margin: 0; + `, + label: css` + max-width: ${theme.spacing(32)}; + `, + description: css` + max-width: ${theme.spacing(80)}; + `, +}); + +interface Props { + register: (path: 'label' | 'description', options?: RegisterOptions) => UseFormRegisterReturn; + readOnly?: boolean; + correlation?: EditFormDTO; +} + +export function CorrelationDetailsFormPart({ register, readOnly = false, correlation }: Props) { + const styles = useStyles2(getStyles); + + return ( + <> + + + + + +