diff --git a/.betterer.results b/.betterer.results index d46bf998089..f5925e1daa7 100644 --- a/.betterer.results +++ b/.betterer.results @@ -77,9 +77,6 @@ exports[`no enzyme tests`] = { "public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:4057721851": [ [1, 19, 13, "RegExp match", "2409514259"] ], - "public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.test.tsx:4128034878": [ - [0, 26, 13, "RegExp match", "2409514259"] - ], "public/app/plugins/datasource/influxdb/components/ConfigEditor.test.tsx:57753101": [ [0, 19, 13, "RegExp match", "2409514259"] ], @@ -1615,11 +1612,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "9"], [0, 0, 0, "Unexpected any. Specify a different type.", "10"], [0, 0, 0, "Unexpected any. Specify a different type.", "11"], - [0, 0, 0, "Unexpected any. Specify a different type.", "12"], - [0, 0, 0, "Unexpected any. Specify a different type.", "13"], - [0, 0, 0, "Unexpected any. Specify a different type.", "14"], - [0, 0, 0, "Unexpected any. Specify a different type.", "15"], - [0, 0, 0, "Unexpected any. Specify a different type.", "16"] + [0, 0, 0, "Unexpected any. Specify a different type.", "12"] ], "packages/grafana-ui/src/components/Select/SelectMenu.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/package.json b/package.json index 0925802ec0a..bb5a47f354e 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "@grafana/tsconfig": "^1.2.0-rc1", "@lingui/cli": "3.14.0", "@lingui/macro": "3.14.0", - "@microsoft/api-extractor": "7.28.6", + "@microsoft/api-extractor": "7.29.5", "@pmmmwh/react-refresh-webpack-plugin": "0.5.7", "@react-types/button": "3.5.1", "@react-types/menu": "3.6.1", @@ -114,7 +114,7 @@ "@testing-library/jest-dom": "5.16.4", "@testing-library/react": "12.1.4", "@testing-library/react-hooks": "8.0.1", - "@testing-library/user-event": "14.3.0", + "@testing-library/user-event": "14.4.3", "@types/angular": "1.8.4", "@types/angular-route": "1.7.2", "@types/classnames": "2.3.0", @@ -189,7 +189,7 @@ "eslint-plugin-jest": "26.6.0", "eslint-plugin-jsdoc": "39.3.3", "eslint-plugin-lodash": "7.4.0", - "eslint-plugin-react": "7.29.4", + "eslint-plugin-react": "7.31.0", "eslint-plugin-react-hooks": "4.6.0", "eslint-webpack-plugin": "3.2.0", "expose-loader": "4.0.0", @@ -404,7 +404,7 @@ "resolutions": { "underscore": "1.13.4", "@types/slate": "0.47.2", - "@microsoft/api-extractor-model": "7.22.1", + "@microsoft/api-extractor-model": "7.23.3", "@rushstack/node-core-library": "3.49.0", "@rushstack/rig-package": "0.3.13", "@rushstack/ts-command-line": "4.12.1", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index c767d6b6aee..017aa1da7b6 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -64,7 +64,7 @@ "@testing-library/jest-dom": "5.16.4", "@testing-library/react": "12.1.4", "@testing-library/react-hooks": "8.0.1", - "@testing-library/user-event": "14.3.0", + "@testing-library/user-event": "14.4.3", "@types/history": "4.7.11", "@types/jest": "28.1.6", "@types/jquery": "3.5.14", 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 boolean; onClear?: () => void; + invalid?: boolean; } /** @@ -186,7 +187,7 @@ export class DataSourcePicker extends PureComponent { if (o.meta && isUnsignedPluginSignature(o.meta.signature) && o !== value) { return ( diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index c3a111a3347..9d1aec7129a 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -130,7 +130,7 @@ "@testing-library/jest-dom": "5.16.4", "@testing-library/react": "12.1.4", "@testing-library/react-hooks": "8.0.1", - "@testing-library/user-event": "14.3.0", + "@testing-library/user-event": "14.4.3", "@types/classnames": "2.3.0", "@types/common-tags": "^1.8.0", "@types/d3": "7.4.0", diff --git a/packages/grafana-ui/src/components/Cascader/Cascader.test.tsx b/packages/grafana-ui/src/components/Cascader/Cascader.test.tsx index c25c1c2397e..d70e1b5c362 100644 --- a/packages/grafana-ui/src/components/Cascader/Cascader.test.tsx +++ b/packages/grafana-ui/src/components/Cascader/Cascader.test.tsx @@ -1,6 +1,5 @@ import { act, render, screen } from '@testing-library/react'; import userEvent, { PointerEventsCheckLevel } from '@testing-library/user-event'; -import { UserEvent } from '@testing-library/user-event/dist/types/setup'; import React from 'react'; import { Cascader, CascaderOption, CascaderProps } from './Cascader'; @@ -47,7 +46,7 @@ describe('Cascader', () => { const placeholder = 'cascader-placeholder'; describe('options from state change', () => { - let user: UserEvent; + let user: ReturnType; beforeEach(() => { jest.useFakeTimers(); diff --git a/packages/grafana-ui/src/components/ConfirmButton/DeleteButton.tsx b/packages/grafana-ui/src/components/ConfirmButton/DeleteButton.tsx index c0e5014d5d9..302f8523bf6 100644 --- a/packages/grafana-ui/src/components/ConfirmButton/DeleteButton.tsx +++ b/packages/grafana-ui/src/components/ConfirmButton/DeleteButton.tsx @@ -13,9 +13,11 @@ export interface Props { /** Disable button click action */ disabled?: boolean; 'aria-label'?: string; + /** Close after delete button is clicked */ + closeOnConfirm?: boolean; } -export const DeleteButton: FC = ({ 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 ( + <> + + + + + +