From fc1a90015318cabdfdf9b0723f0ee21515980fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ida=20=C5=A0tambuk?= Date: Thu, 26 Oct 2023 13:04:47 +0200 Subject: [PATCH 1/4] Migrate query editor and config editor --- package.json | 3 +- src/components/ConfigEditor/ConfigEditor.tsx | 21 +- .../QueryEditor/AccountIdDropdown.tsx | 18 +- .../QueryEditor/QueryEditor.test.tsx | 374 +++++++++--------- src/components/QueryEditor/QueryEditor.tsx | 12 +- .../QueryEditor/QueryEditorForm.tsx | 341 ++++++++-------- .../QueryEditor/QueryEditorFormOld.tsx | 267 +++++++++++++ src/components/QueryEditor/QuerySection.tsx | 77 ++-- .../QueryEditor/QuerySectionOld.tsx | 82 ++++ src/components/QueryEditor/XrayLinks.tsx | 24 +- src/components/QueryEditor/XrayLinksOld.tsx | 40 ++ src/components/QueryEditor/constants.ts | 95 ++++- yarn.lock | 27 +- 13 files changed, 960 insertions(+), 421 deletions(-) create mode 100644 src/components/QueryEditor/QueryEditorFormOld.tsx create mode 100644 src/components/QueryEditor/QuerySectionOld.tsx create mode 100644 src/components/QueryEditor/XrayLinksOld.tsx diff --git a/package.json b/package.json index 7c94a6e..d5b60cb 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "devDependencies": { "@babel/core": "^7.16.7", "@emotion/css": "^11.1.3", - "@grafana/aws-sdk": "0.1.2", + "@grafana/aws-sdk": "0.3.0", "@grafana/data": "9.3.2", "@grafana/e2e": "9.3.2", "@grafana/e2e-selectors": "9.3.2", @@ -76,6 +76,7 @@ "webpack-livereload-plugin": "^3.0.2" }, "dependencies": { + "@grafana/experimental": "1.7.3", "react-use": "^15.3.4", "tslib": "^2.2.0" } diff --git a/src/components/ConfigEditor/ConfigEditor.tsx b/src/components/ConfigEditor/ConfigEditor.tsx index df365be..581fb08 100644 --- a/src/components/ConfigEditor/ConfigEditor.tsx +++ b/src/components/ConfigEditor/ConfigEditor.tsx @@ -1,17 +1,18 @@ import { AwsAuthDataSourceJsonData, AwsAuthDataSourceSecureJsonData, ConnectionConfig } from '@grafana/aws-sdk'; import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; -import React, { PureComponent } from 'react'; +import React from 'react'; import { standardRegions } from './regions'; +import { config } from '@grafana/runtime'; export type Props = DataSourcePluginOptionsEditorProps; -export class ConfigEditor extends PureComponent { - render() { - return ( -
- - {/* can add x-ray specific things here */} -
- ); - } +export function ConfigEditor(props: Props) { + const newFormStylingEnabled = config.featureToggles.awsDatasourcesNewFormStyling; + + return ( +
+ + {/* can add x-ray specific things here */} +
+ ); } diff --git a/src/components/QueryEditor/AccountIdDropdown.tsx b/src/components/QueryEditor/AccountIdDropdown.tsx index 5f20829..3ebb494 100644 --- a/src/components/QueryEditor/AccountIdDropdown.tsx +++ b/src/components/QueryEditor/AccountIdDropdown.tsx @@ -5,10 +5,11 @@ import { TimeRange } from '@grafana/data'; import { config } from '@grafana/runtime'; import React from 'react'; import { InlineFormLabel, MultiSelect } from '@grafana/ui'; - +import { EditorField } from '@grafana/experimental'; type Props = { datasource: XrayDataSource; query: XrayQuery; + newFornStylingEnabled?: boolean; range?: TimeRange; onChange: (items: string[]) => void; }; @@ -22,7 +23,20 @@ export const AccountIdDropdown = (props: Props) => { return null; } - return ( + return props.newFornStylingEnabled ? ( + + ({ + value: accountId, + label: accountId, + }))} + value={props.query.accountIds} + onChange={(items) => props.onChange(items.map((item) => item.value || ''))} + placeholder={'All'} + /> + + ) : (
AccountId diff --git a/src/components/QueryEditor/QueryEditor.test.tsx b/src/components/QueryEditor/QueryEditor.test.tsx index 7441b95..6856027 100644 --- a/src/components/QueryEditor/QueryEditor.test.tsx +++ b/src/components/QueryEditor/QueryEditor.test.tsx @@ -6,32 +6,6 @@ import { XrayDataSource } from '../../DataSource'; import { DataSourceInstanceSettings, ScopedVars, TypedVariableModel } from '@grafana/data'; import * as grafanaRuntime from '@grafana/runtime'; -jest.spyOn(grafanaRuntime, 'getTemplateSrv').mockImplementation(() => { - return { - getVariables(): TypedVariableModel[] { - return []; - }, - replace(target?: string, scopedVars?: ScopedVars, format?: string | Function): string { - if (!target) { - return ''; - } - const vars: Record = { - ...scopedVars, - someVar: { - value: '200', - }, - }; - for (const key of Object.keys(vars)) { - target = target!.replace(`\$${key}`, vars[key].value); - target = target!.replace(`\${${key}}`, vars[key].value); - } - return target!; - }, - containsTemplate: jest.fn(), - updateTimeRange: jest.fn(), - }; -}); - const defaultProps = { onRunQuery: undefined as any, datasource: { @@ -75,6 +49,38 @@ jest.mock( { virtual: true } ); +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getTemplateSrv: () => ({ + getVariables(): TypedVariableModel[] { + return []; + }, + replace(target?: string, scopedVars?: ScopedVars, format?: string | Function): string { + if (!target) { + return ''; + } + const vars: Record = { + ...scopedVars, + someVar: { + value: '200', + }, + }; + for (const key of Object.keys(vars)) { + target = target!.replace(`\$${key}`, vars[key].value); + target = target!.replace(`\${${key}}`, vars[key].value); + } + return target!; + }, + containsTemplate: jest.fn(), + updateTimeRange: jest.fn(), + }), + config: { + featureToggles: { + awsDatasourcesNewFormStylingEnabled: false, + }, + }, +})); + async function renderWithQuery(query: Omit, rerender?: any) { const renderFunc = rerender || render; @@ -100,177 +106,185 @@ async function renderWithQuery(query: Omit, rerender?: any) } describe('QueryEditor', () => { - it.each([ - [XrayQueryType.getTrace, 'Trace List'], - [XrayQueryType.getTraceSummaries, 'Trace List'], - [XrayQueryType.getTimeSeriesServiceStatistics, 'Trace Statistics'], - [XrayQueryType.getAnalyticsRootCauseResponseTimeService, 'Root Cause'], - [XrayQueryType.getAnalyticsRootCauseResponseTimePath, 'Path'], - [XrayQueryType.getAnalyticsRootCauseErrorService, 'Root Cause'], - [XrayQueryType.getAnalyticsRootCauseErrorPath, 'Path'], - [XrayQueryType.getAnalyticsRootCauseErrorMessage, 'Error Message'], - [XrayQueryType.getAnalyticsRootCauseFaultService, 'Root Cause'], - [XrayQueryType.getAnalyticsRootCauseFaultPath, 'Path'], - [XrayQueryType.getAnalyticsRootCauseFaultMessage, 'Error Message'], - [XrayQueryType.getAnalyticsUser, 'End user impact'], - [XrayQueryType.getAnalyticsUrl, 'URL'], - [XrayQueryType.getAnalyticsStatusCode, 'HTTP status code'], - [XrayQueryType.getInsights, 'Insights'], - [XrayQueryType.getServiceMap, 'Service Map'], - ])('renders proper query type option when query type is %s', async (type, expected) => { - await renderWithQuery({ - query: 'test query', - queryType: type as XrayQueryType, - }); - expect(screen.getByText(expected)).not.toBeNull(); - }); + function run(testName: string) { + describe(testName, () => { + it.each([ + [XrayQueryType.getTrace, 'Trace List'], + [XrayQueryType.getTraceSummaries, 'Trace List'], + [XrayQueryType.getTimeSeriesServiceStatistics, 'Trace Statistics'], + [XrayQueryType.getAnalyticsRootCauseResponseTimeService, 'Root Cause'], + [XrayQueryType.getAnalyticsRootCauseResponseTimePath, 'Path'], + [XrayQueryType.getAnalyticsRootCauseErrorService, 'Root Cause'], + [XrayQueryType.getAnalyticsRootCauseErrorPath, 'Path'], + [XrayQueryType.getAnalyticsRootCauseErrorMessage, 'Error Message'], + [XrayQueryType.getAnalyticsRootCauseFaultService, 'Root Cause'], + [XrayQueryType.getAnalyticsRootCauseFaultPath, 'Path'], + [XrayQueryType.getAnalyticsRootCauseFaultMessage, 'Error Message'], + [XrayQueryType.getAnalyticsUser, 'End user impact'], + [XrayQueryType.getAnalyticsUrl, 'URL'], + [XrayQueryType.getAnalyticsStatusCode, 'HTTP status code'], + [XrayQueryType.getInsights, 'Insights'], + [XrayQueryType.getServiceMap, 'Service Map'], + ])('renders proper query type option when query type is %s', async (type, expected) => { + await renderWithQuery({ + query: 'test query', + queryType: type as XrayQueryType, + }); + expect(screen.getByDisplayValue(expected)).not.toBeNull(); + }); - it('inits the query with query type', async () => { - const { onChange } = await renderWithQuery({ query: '' }); - expect(onChange).toBeCalledWith({ - refId: 'A', - query: '', - queryType: XrayQueryType.getTraceSummaries, - region: 'default', - group: { GroupName: 'Default', GroupARN: 'DefaultARN' }, - }); - }); + it('inits the query with query type', async () => { + const { onChange } = await renderWithQuery({ query: '' }); + expect(onChange).toBeCalledWith({ + refId: 'A', + query: '', + queryType: XrayQueryType.getTraceSummaries, + region: 'default', + group: { GroupName: 'Default', GroupARN: 'DefaultARN' }, + }); + }); - it('shows column filter and resolution only if query type is getTimeSeriesServiceStatistics', async () => { - const { rerender } = await renderWithQuery({ query: '', queryType: XrayQueryType.getTraceSummaries }); - expect(screen.queryByTestId('column-filter')).toBeNull(); - expect(screen.queryByTestId('resolution')).toBeNull(); + it('shows column filter and resolution only if query type is getTimeSeriesServiceStatistics', async () => { + const { rerender } = await renderWithQuery({ query: '', queryType: XrayQueryType.getTraceSummaries }); + expect(screen.queryByTestId('column-filter')).toBeNull(); + expect(screen.queryByTestId('resolution')).toBeNull(); - await renderWithQuery({ query: '', queryType: XrayQueryType.getTimeSeriesServiceStatistics }, rerender); - expect(screen.queryByTestId('column-filter')).not.toBeNull(); - expect(screen.queryByTestId('resolution')).not.toBeNull(); - }); + await renderWithQuery({ query: '', queryType: XrayQueryType.getTimeSeriesServiceStatistics }, rerender); + expect(screen.queryByTestId('column-filter')).not.toBeNull(); + expect(screen.queryByTestId('resolution')).not.toBeNull(); + }); - it('hides query input if query is service map', async () => { - await renderWithQuery({ query: '', queryType: XrayQueryType.getServiceMap }); - expect(screen.queryByText(/^Query$/)).toBeNull(); - }); + it('hides query input if query is service map', async () => { + await renderWithQuery({ query: '', queryType: XrayQueryType.getServiceMap }); + expect(screen.queryByText(/^Query$/)).toBeNull(); + }); - it('correctly changes the query type if user fills in trace id', async () => { - const { onChange } = await renderWithQuery({ query: '', queryType: XrayQueryType.getTraceSummaries }); + it('correctly changes the query type if user fills in trace id', async () => { + const { onChange } = await renderWithQuery({ query: '', queryType: XrayQueryType.getTraceSummaries }); - const field = screen.getByTestId('query-field-mock'); + const field = screen.getByTestId('query-field-mock'); - fireEvent.change(field, { target: { value: '1-5f160a8b-83190adad07f429219c0e259' } }); + fireEvent.change(field, { target: { value: '1-5f160a8b-83190adad07f429219c0e259' } }); - expect(onChange.mock.calls[1][0]).toEqual({ - refId: 'A', - query: '1-5f160a8b-83190adad07f429219c0e259', - queryType: XrayQueryType.getTrace, - }); - }); + expect(onChange.mock.calls[1][0]).toEqual({ + refId: 'A', + query: '1-5f160a8b-83190adad07f429219c0e259', + queryType: XrayQueryType.getTrace, + }); + }); - it('can add and remove column filters', async () => { - let { onChange } = await renderWithQuery({ - query: '', - columns: [], - queryType: XrayQueryType.getTimeSeriesServiceStatistics, - }); + it('can add and remove column filters', async () => { + let { onChange } = await renderWithQuery({ + query: '', + columns: [], + queryType: XrayQueryType.getTimeSeriesServiceStatistics, + }); - let select = screen.getByText('All columns'); - fireEvent.mouseDown(select); - let option = screen.getByText(/Success Count/i); - fireEvent.click(option); + let select = screen.getByText('All columns'); + fireEvent.mouseDown(select); + let option = screen.getByText(/Success Count/i); + fireEvent.click(option); - expect(onChange).toBeCalledWith({ - refId: 'A', - query: '', - columns: ['OkCount'], - queryType: XrayQueryType.getTimeSeriesServiceStatistics, - }); - }); + expect(onChange).toBeCalledWith({ + refId: 'A', + query: '', + columns: ['OkCount'], + queryType: XrayQueryType.getTimeSeriesServiceStatistics, + }); + }); - it('waits until groups and regions are loaded', async () => { - await act(async () => { - render( - {}} - /> - ); - // No ideal selector but spinner does not seem to have any better thing to select by - expect(screen.getByText('', { selector: '.fa-spinner' })).toBeDefined(); - await waitFor(() => expect(screen.getByText('Query')).toBeDefined()); - }); - }); + it('waits until groups and regions are loaded', async () => { + await act(async () => { + render( + {}} + /> + ); + // No ideal selector but spinner does not seem to have any better thing to select by + expect(screen.getByText('', { selector: '.fa-spinner' })).toBeDefined(); + await waitFor(() => expect(screen.getByText('Query')).toBeDefined()); + }); + }); - it('sets the correct links based on region default', async () => { - renderEditorWithRegion('region1', 'default'); - await checkLinks({ - console: 'https://region1.console.aws.amazon.com/xray/home?region=region1#/analytics', - serviceMap: 'https://region1.console.aws.amazon.com/xray/home?region=region1#/service-map/', - }); - }); + it('sets the correct links based on region default', async () => { + renderEditorWithRegion('region1', 'default'); + await checkLinks({ + console: 'https://region1.console.aws.amazon.com/xray/home?region=region1#/analytics', + serviceMap: 'https://region1.console.aws.amazon.com/xray/home?region=region1#/service-map/', + }); + }); - it('sets the correct links based on region in query', async () => { - renderEditorWithRegion('region1', 'region2'); - await checkLinks({ - console: 'https://region2.console.aws.amazon.com/xray/home?region=region2#/analytics', - serviceMap: 'https://region2.console.aws.amazon.com/xray/home?region=region2#/service-map/', - }); - }); + it('sets the correct links based on region in query', async () => { + renderEditorWithRegion('region1', 'region2'); + await checkLinks({ + console: 'https://region2.console.aws.amazon.com/xray/home?region=region2#/analytics', + serviceMap: 'https://region2.console.aws.amazon.com/xray/home?region=region2#/service-map/', + }); + }); - it('shows the accountIds in a dropdown on service map selection', async () => { - const mockGetAccountIdsForServiceMap = jest.fn(() => Promise.resolve(['account1', 'account2'])); - await act(async () => { - render( - {}} - /> - ); - expect(screen.getByText('', { selector: '.fa-spinner' })).toBeDefined(); - await waitFor(() => expect(screen.getByText('account1')).toBeDefined()); - expect(mockGetAccountIdsForServiceMap).toHaveBeenCalled(); - }); - }); + it('shows the accountIds in a dropdown on service map selection', async () => { + const mockGetAccountIdsForServiceMap = jest.fn(() => Promise.resolve(['account1', 'account2'])); + await act(async () => { + render( + {}} + /> + ); + expect(screen.getByText('', { selector: '.fa-spinner' })).toBeDefined(); + await waitFor(() => expect(screen.getByText('account1')).toBeDefined()); + expect(mockGetAccountIdsForServiceMap).toHaveBeenCalled(); + }); + }); - it('does not fetch account ids if service map is not selected', async () => { - const mockGetAccountIdsForServiceMap = jest.fn(() => Promise.resolve(['account1', 'account2'])); - await act(async () => { - render( - {}} - /> - ); - expect(screen.getByText('', { selector: '.fa-spinner' })).toBeDefined(); - await waitForElementToBeRemoved(() => screen.getByText('', { selector: '.fa-spinner' })); - expect(mockGetAccountIdsForServiceMap).not.toHaveBeenCalled(); + it('does not fetch account ids if service map is not selected', async () => { + const mockGetAccountIdsForServiceMap = jest.fn(() => Promise.resolve(['account1', 'account2'])); + await act(async () => { + render( + {}} + /> + ); + expect(screen.getByText('', { selector: '.fa-spinner' })).toBeDefined(); + await waitForElementToBeRemoved(() => screen.getByText('', { selector: '.fa-spinner' })); + expect(mockGetAccountIdsForServiceMap).not.toHaveBeenCalled(); + }); + }); }); - }); + } + + run('QueryEditorForm with awsDatasourcesNewFormStyling disabled'); + grafanaRuntime.config.featureToggles.awsDatasourcesNewFormStyling = true; + run('QueryEditorForm with awsDatasourcesNewFormStyling enabled'); }); function makeDataSource(settings: DataSourceInstanceSettings) { diff --git a/src/components/QueryEditor/QueryEditor.tsx b/src/components/QueryEditor/QueryEditor.tsx index 3e2606c..bd0f8a5 100644 --- a/src/components/QueryEditor/QueryEditor.tsx +++ b/src/components/QueryEditor/QueryEditor.tsx @@ -1,8 +1,10 @@ import React from 'react'; import { Spinner } from '@grafana/ui'; import { useGroups } from './useGroups'; -import { QueryEditorForm, XrayQueryEditorFormProps } from './QueryEditorForm'; import { useRegions } from './useRegions'; +import { QueryEditorFormOld, XrayQueryEditorFormProps } from './QueryEditorFormOld'; +import { config } from '@grafana/runtime'; +import { QueryEditorForm } from './QueryEditorForm'; /** * Simple wrapper that is only responsible to load groups and delay actual render of the QueryEditorForm. Main reason * for that is that there is queryInit code that requires groups to be already loaded and is separate hook and it @@ -10,6 +12,8 @@ import { useRegions } from './useRegions'; * alternatives. */ export function QueryEditor(props: Omit) { + const newFormStylingEnabled = config.featureToggles.awsDatasourcesNewFormStyling; + const regions = useRegions(props.datasource); // Use groups will return old groups after region change so it does not flash loading state. in case datasource // changes regions will return undefined so that will do the loading state. @@ -20,6 +24,10 @@ export function QueryEditor(props: Omit; } else { - return ; + return newFormStylingEnabled ? ( + + ) : ( + + ); } } diff --git a/src/components/QueryEditor/QueryEditorForm.tsx b/src/components/QueryEditor/QueryEditorForm.tsx index 45bd4ee..8212902 100644 --- a/src/components/QueryEditor/QueryEditorForm.tsx +++ b/src/components/QueryEditor/QueryEditorForm.tsx @@ -1,41 +1,39 @@ import React from 'react'; import { css } from '@emotion/css'; import { QueryEditorProps, ScopedVars } from '@grafana/data'; -import { ButtonCascader, InlineFormLabel, MultiSelect, Segment, stylesFactory, Select } from '@grafana/ui'; +import { MultiSelect, Select, Cascader } from '@grafana/ui'; import { Group, Region, XrayJsonData, XrayQuery, XrayQueryType } from '../../types'; import { useInitQuery } from './useInitQuery'; import { + QueryTypeOption, columnNames, dummyAllGroup, insightsOption, - QueryTypeOption, queryTypeOptions, serviceMapOption, traceListOption, traceStatisticsOption, } from './constants'; import { XrayDataSource } from '../../DataSource'; -import { QuerySection } from './QuerySection'; -import { XrayLinks } from './XrayLinks'; import { getTemplateSrv } from '@grafana/runtime'; import { AccountIdDropdown } from './AccountIdDropdown'; +import { EditorRow, EditorFieldGroup, EditorField } from '@grafana/experimental'; +import { QuerySection } from './QuerySection'; +import { XrayLinks } from './XrayLinks'; -function findOptionForQueryType(queryType: XrayQueryType, options: any = queryTypeOptions): QueryTypeOption[] { +function findOptionForQueryType(queryType: XrayQueryType, options: any = queryTypeOptions): QueryTypeOption | null { for (const option of options) { - const selected: QueryTypeOption[] = []; if (option.queryType === queryType) { - selected.push(option); - return selected; + return option; } - if (option.children) { - const found = findOptionForQueryType(queryType, option.children); - if (found.length) { - selected.push(option, ...found); - return selected; + if (option.items) { + const found = findOptionForQueryType(queryType, option.items); + if (found) { + return found; } } } - return []; + return null; } /** @@ -43,37 +41,62 @@ function findOptionForQueryType(queryType: XrayQueryType, options: any = queryTy * between trace list and single trace and we detect that based on query. So trace list option returns single trace * if query contains single traceID. */ -export function queryTypeToQueryTypeOptions(queryType?: XrayQueryType): QueryTypeOption[] { +export function queryTypeToQueryTypeOptions(queryType?: XrayQueryType): QueryTypeOption | null { if (!queryType || queryType === XrayQueryType.getTimeSeriesServiceStatistics) { - return [traceStatisticsOption]; + return traceStatisticsOption; } if (queryType === XrayQueryType.getTrace || queryType === XrayQueryType.getTraceSummaries) { - return [traceListOption]; + return traceListOption; } if (queryType === XrayQueryType.getInsights) { - return [insightsOption]; + return insightsOption; } return findOptionForQueryType(queryType); } +// recursively search for the selected option in cascade option's item or item.items +export function findQueryTypeOption(options: QueryTypeOption[], selected: string): QueryTypeOption | undefined { + for (const option of options) { + // Check if the current option's value matches the selected value + if (option.value === selected) { + return option; + } + + // If no match was found at the current level, check items if they exist + if (option.items) { + const result = findQueryTypeOption(option.items, selected); + if (result) { + return result; + } + } + } -export function queryTypeOptionToQueryType(selected: string[], query: string, scopedVars?: ScopedVars): XrayQueryType { - if (selected[0] === traceListOption.value) { + // If no match was found in the current array or its items, return undefined + return undefined; +} + +export function queryTypeOptionToQueryType( + selected: string, + query: string, + scopedVars?: ScopedVars +): XrayQueryType | undefined { + if (selected === traceListOption.value) { const resolvedQuery = getTemplateSrv().replace(query, scopedVars); const isTraceIdQuery = /^\d-\w{8}-\w{24}$/.test(resolvedQuery.trim()); return isTraceIdQuery ? XrayQueryType.getTrace : XrayQueryType.getTraceSummaries; } else { - let found: any = undefined; - for (const path of selected) { - found = (found?.children ?? queryTypeOptions).find((option: QueryTypeOption) => option.value === path); + const foundItem = findQueryTypeOption(queryTypeOptions, selected); + if (!foundItem) { + console.log('item could not be found in the options'); } - return found.queryType; + console.log(JSON.stringify(foundItem, null, 2)); + return foundItem?.queryType ?? undefined; } } -const getStyles = stylesFactory(() => ({ +const getStyles = () => ({ queryParamsRow: css` flex-wrap: wrap; `, @@ -83,7 +106,10 @@ const getStyles = stylesFactory(() => ({ regionSelect: css` margin-right: 4px; `, -})); + formFieldStyles: css({ + marginBottom: 0, + }), +}); export type XrayQueryEditorFormProps = QueryEditorProps & { groups: Group[]; @@ -102,104 +128,81 @@ export function QueryEditorForm({ const allRegions = [{ label: 'default', value: 'default', text: 'default' }, ...regions]; useInitQuery(query, onChange, groups, allRegions, datasource); - const selectedOptions = queryTypeToQueryTypeOptions(query.queryType); - const allGroups = selectedOptions[0] === insightsOption ? [dummyAllGroup, ...groups] : groups; + const selectedOption = queryTypeToQueryTypeOptions(query.queryType); + const allGroups = selectedOption === insightsOption ? [dummyAllGroup, ...groups] : groups; const styles = getStyles(); return ( -
- {![insightsOption, serviceMapOption].includes(selectedOptions[0]) && ( -
- -
- )} -
-
- - Region - - + onChange({ + ...query, + region: v.value, + }) + } + width={18} + placeholder="Choose Region" + menuPlacement="bottom" + maxMenuHeight={500} + /> + + + ({ value: val, label: val }))} onChange={(value) => { @@ -209,17 +212,17 @@ export function QueryEditorForm({ }); }} /> -
+ )} -
- -
- {selectedOptions[0] === traceStatisticsOption && ( -
- - Resolution - - + + onChange({ + ...query, + region: v.value, + }) + } + width={18} + placeholder="Choose Region" + menuPlacement="bottom" + maxMenuHeight={500} + /> +
+
+ + Query Type + + option.value)} + options={queryTypeOptionsOld} + onChange={(value) => { + const newQueryType = queryTypeOptionToQueryType(value, query.query || '', data?.request?.scopedVars); + onChange({ + ...query, + queryType: newQueryType, + columns: newQueryType === XrayQueryType.getTimeSeriesServiceStatistics ? ['all'] : undefined, + } as any); + }} + > + {selectedOptions[selectedOptions.length - 1].label} + +
+ +
+ + Group + + ({ + value: group.GroupARN, + label: group.GroupName, + }))} + onChange={(value) => { + onChange({ + ...query, + group: allGroups.find((g: Group) => g.GroupARN === value.value), + } as any); + }} + /> +
+ + {[serviceMapOption].includes(selectedOptions[0]) && ( + + onChange({ + ...query, + accountIds, + }) + } + /> + )} + +
+ {selectedOptions[0] === insightsOption && ( +
+ + State + + ({ value: val, label: val }))} + onChange={(value) => { + onChange({ + ...query, + state: value.value, + }); + }} + /> +
+ )} +
+ +
+ {selectedOptions[0] === traceStatisticsOption && ( +
+ + Resolution + + ({ value: val, label: val }))} + onChange={({ value }) => { + onChange({ + ...query, + resolution: value === 'auto' ? undefined : parseInt(value!, 10), + } as any); + }} + /> +
+ )} +
+ + {/* spring to push the sections apart */} +
+ +
+ {selectedOptions[0] === traceStatisticsOption && ( +
+ + Columns + +
+ ({ + label: columnNames[c], + value: c, + }))} + value={(query.columns || []).map((c) => ({ + label: columnNames[c], + value: c, + }))} + onChange={(values) => onChange({ ...query, columns: values.map((v) => v.value!) })} + closeMenuOnSelect={false} + isClearable={true} + placeholder="All columns" + menuPlacement="bottom" + /> +
+
+ )} +
+ ); +} diff --git a/src/components/QueryEditor/QuerySection.tsx b/src/components/QueryEditor/QuerySection.tsx index 58f1835..de9dc0f 100644 --- a/src/components/QueryEditor/QuerySection.tsx +++ b/src/components/QueryEditor/QuerySection.tsx @@ -1,32 +1,31 @@ -import { Icon, InlineFormLabel, stylesFactory, Tooltip } from '@grafana/ui'; import { XRayQueryField } from './XRayQueryField'; import React from 'react'; -import { queryTypeOptionToQueryType } from './QueryEditorForm'; import { XrayDataSource } from '../../DataSource'; import { css } from '@emotion/css'; import { XrayQuery } from '../../types'; import { QueryTypeOption } from './constants'; +import { EditorField } from '@grafana/experimental'; +import { queryTypeOptionToQueryType } from './QueryEditorForm'; -const getStyles = stylesFactory(() => ({ - tooltipLink: css` - color: #33a2e5; - &:hover { - color: #33a2e5; - filter: brightness(120%); - } - `, -})); +const styles = { + tooltipLink: css({ + color: '#33a2e5', + '&:hover': { + color: ' #33a2e5', + filter: 'brightness(120%)', + }, + }), +}; type Props = { query: XrayQuery; datasource: XrayDataSource; onChange: (value: XrayQuery) => void; onRunQuery: () => void; - selectedOptions: QueryTypeOption[]; + selectedOption: QueryTypeOption; }; export function QuerySection(props: Props) { - const { datasource, query, onRunQuery, onChange, selectedOptions } = props; - const styles = getStyles(); + const { datasource, query, onRunQuery, onChange, selectedOption } = props; const onRunQueryLocal = () => { onChange(query); @@ -37,30 +36,25 @@ export function QuerySection(props: Props) { }; return ( -
- - Query  - - See{' '} - - X-Ray documentation - {' '} - for filter expression help. - - } - theme="info" - > - - - + + See{' '} + + X-Ray documentation + {' '} + for filter expression help. + + } + > { onChange({ ...query, - queryType: queryTypeOptionToQueryType( - selectedOptions.map((option) => option.value), - e.query - ), + queryType: queryTypeOptionToQueryType(selectedOption.value, e.query), query: e.query, }); }} /> -
+ ); } diff --git a/src/components/QueryEditor/QuerySectionOld.tsx b/src/components/QueryEditor/QuerySectionOld.tsx new file mode 100644 index 0000000..0df05dd --- /dev/null +++ b/src/components/QueryEditor/QuerySectionOld.tsx @@ -0,0 +1,82 @@ +import { Icon, InlineFormLabel, stylesFactory, Tooltip } from '@grafana/ui'; +import { XRayQueryField } from './XRayQueryField'; +import React from 'react'; +import { XrayDataSource } from '../../DataSource'; +import { css } from '@emotion/css'; +import { XrayQuery } from '../../types'; +import { QueryTypeOption } from './constants'; +import { queryTypeOptionToQueryType } from './QueryEditorFormOld'; + +const getStyles = stylesFactory(() => ({ + tooltipLink: css` + color: #33a2e5; + &:hover { + color: #33a2e5; + filter: brightness(120%); + } + `, +})); + +type Props = { + query: XrayQuery; + datasource: XrayDataSource; + onChange: (value: XrayQuery) => void; + onRunQuery: () => void; + selectedOptions: QueryTypeOption[]; +}; +export function QuerySectionOld(props: Props) { + const { datasource, query, onRunQuery, onChange, selectedOptions } = props; + const styles = getStyles(); + + const onRunQueryLocal = () => { + onChange(query); + // Only run query if it has value + if (query.query) { + onRunQuery(); + } + }; + + return ( +
+ + Query  + + See{' '} + + X-Ray documentation + {' '} + for filter expression help. + + } + theme="info" + > + + + + { + onChange({ + ...query, + queryType: queryTypeOptionToQueryType( + selectedOptions.map((option) => option.value), + e.query + ), + query: e.query, + }); + }} + /> +
+ ); +} diff --git a/src/components/QueryEditor/XrayLinks.tsx b/src/components/QueryEditor/XrayLinks.tsx index a87ef2a..ac2b8da 100644 --- a/src/components/QueryEditor/XrayLinks.tsx +++ b/src/components/QueryEditor/XrayLinks.tsx @@ -1,25 +1,27 @@ -import { stylesFactory } from '@grafana/ui'; +import { useTheme2 } from '@grafana/ui'; import { css } from '@emotion/css'; import { XrayDataSource } from '../../DataSource'; import { XrayQuery } from '../../types'; -import { TimeRange } from '@grafana/data'; +import { GrafanaTheme2, TimeRange } from '@grafana/data'; import React from 'react'; -const getStyles = stylesFactory(() => ({ - container: css` - display: flex; - `, - link: css` - white-space: nowrap; - `, -})); +const getStyles = (theme: GrafanaTheme2) => ({ + container: css({ display: 'flex', paddingTop: theme.spacing(3) }), + link: css({ + whiteSpace: 'nowrap', + backgroundColor: theme.colors.background.primary, + color: theme.colors.text.primary, + }), +}); type XrayLinksProps = { datasource: XrayDataSource; query: XrayQuery; range?: TimeRange; }; export function XrayLinks({ datasource, query, range }: XrayLinksProps) { - const styles = getStyles(); + const theme = useTheme2(); + const styles = getStyles(theme); + return (
{[ diff --git a/src/components/QueryEditor/XrayLinksOld.tsx b/src/components/QueryEditor/XrayLinksOld.tsx new file mode 100644 index 0000000..90fc8ad --- /dev/null +++ b/src/components/QueryEditor/XrayLinksOld.tsx @@ -0,0 +1,40 @@ +import { stylesFactory } from '@grafana/ui'; +import { css } from '@emotion/css'; +import { XrayDataSource } from '../../DataSource'; +import { XrayQuery } from '../../types'; +import { TimeRange } from '@grafana/data'; +import React from 'react'; + +const getStyles = stylesFactory(() => ({ + container: css` + display: flex; + `, + link: css` + white-space: nowrap; + `, +})); +type XrayLinksProps = { + datasource: XrayDataSource; + query: XrayQuery; + range?: TimeRange; +}; +export function XrayLinksOld({ datasource, query, range }: XrayLinksProps) { + const styles = getStyles(); + return ( +
+ {[ + ['To X-Ray service map', datasource.getServiceMapUrl(query.region)], + ['Open in X-Ray console', datasource.getXrayUrlForQuery(query, range)], + ].map(([text, href]) => { + return ( + + + +  {text} + + + ); + })} +
+ ); +} diff --git a/src/components/QueryEditor/constants.ts b/src/components/QueryEditor/constants.ts index be47195..30f7577 100644 --- a/src/components/QueryEditor/constants.ts +++ b/src/components/QueryEditor/constants.ts @@ -4,6 +4,7 @@ import { CascaderOption } from '@grafana/ui'; export type QueryTypeOption = CascaderOption & { queryType?: XrayQueryType; children?: QueryTypeOption[]; + items?: QueryTypeOption[]; }; export const traceListOption: QueryTypeOption = { label: 'Trace List', value: 'traceList' }; @@ -24,7 +25,7 @@ export const traceStatisticsOption: QueryTypeOption = { queryType: XrayQueryType.getTimeSeriesServiceStatistics, }; -export const queryTypeOptions: QueryTypeOption[] = [ +export const queryTypeOptionsOld: QueryTypeOption[] = [ traceListOption, traceStatisticsOption, insightsOption, @@ -116,6 +117,98 @@ export const queryTypeOptions: QueryTypeOption[] = [ serviceMapOption, ]; +export const queryTypeOptions: QueryTypeOption[] = [ + traceListOption, + traceStatisticsOption, + insightsOption, + { + label: 'Trace Analytics', + value: 'traceAnalytics', + items: [ + { + value: 'rootCause', + label: 'Root Cause', + items: [ + { + value: 'responseTime', + label: 'Response Time', + items: [ + { + value: 'rootCauseService', + label: 'Root Cause', + queryType: XrayQueryType.getAnalyticsRootCauseResponseTimeService, + } as QueryTypeOption, + { + value: 'path', + label: 'Path', + queryType: XrayQueryType.getAnalyticsRootCauseResponseTimePath, + }, + ], + }, + { + value: 'error', + label: 'Error', + items: [ + { + value: 'rootCauseService', + label: 'Root Cause', + queryType: XrayQueryType.getAnalyticsRootCauseErrorService, + }, + { + value: 'path', + label: 'Path', + queryType: XrayQueryType.getAnalyticsRootCauseErrorPath, + }, + { + value: 'message', + label: 'Error Message', + queryType: XrayQueryType.getAnalyticsRootCauseErrorMessage, + }, + ], + }, + { + value: 'fault', + label: 'Fault', + items: [ + { + value: 'rootCauseService', + label: 'Root Cause', + queryType: XrayQueryType.getAnalyticsRootCauseFaultService, + }, + { + value: 'path', + label: 'Path', + queryType: XrayQueryType.getAnalyticsRootCauseFaultPath, + }, + { + value: 'message', + label: 'Error Message', + queryType: XrayQueryType.getAnalyticsRootCauseFaultMessage, + }, + ], + }, + ], + }, + { + value: 'user', + label: 'End user impact', + queryType: XrayQueryType.getAnalyticsUser, + } as QueryTypeOption, + { + value: 'url', + label: 'URL', + queryType: XrayQueryType.getAnalyticsUrl, + }, + { + value: 'statusCode', + label: 'HTTP status code', + queryType: XrayQueryType.getAnalyticsStatusCode, + }, + ], + }, + serviceMapOption, +]; + export const columnNames: { [key: string]: string } = { 'ErrorStatistics.ThrottleCount': 'Throttle Count', 'ErrorStatistics.TotalCount': 'Error Count', diff --git a/yarn.lock b/yarn.lock index d312a03..1444f6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1621,13 +1621,13 @@ dependencies: tslib "^2.4.1" -"@grafana/aws-sdk@0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@grafana/aws-sdk/-/aws-sdk-0.1.2.tgz#5eda0c2814b91610dddc51510f81ae141f44030b" - integrity sha512-IZDBFWHPuX8LotJl26RMzHu6gLX6e3SE132EYgDwHSSMQ6fZLFY/t1fy39flG119CLckvsg2oIuKdctrgwVowA== +"@grafana/aws-sdk@0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@grafana/aws-sdk/-/aws-sdk-0.3.0.tgz#302fb8841ba870eb89a9195fa6fcb891604539ae" + integrity sha512-UyldLMZLoUwYg77VD/hqbXkfrtbh2osQT3WNpybtHQwLwa1qCaIQB7yntVBv1g+eevrOFnhItqOJRMJoXxBYEQ== dependencies: "@grafana/async-query-data" "0.1.4" - "@grafana/experimental" "1.1.0" + "@grafana/experimental" "1.7.0" "@grafana/data@9.3.2": version "9.3.2" @@ -1708,14 +1708,23 @@ eslint-plugin-react-hooks "4.6.0" typescript "4.8.4" -"@grafana/experimental@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@grafana/experimental/-/experimental-1.1.0.tgz#36b9644b1e61c782ed42b4805c5e297f2cc3f8bf" - integrity sha512-pQhYhw+jB7Q+t8rLcd1jcx91BiFDNslBATJkNIgO9I2Bah+ww+2RH1hUGVoJNPL84vW7WRU7w9k/L7FJs7/L6Q== +"@grafana/experimental@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@grafana/experimental/-/experimental-1.7.0.tgz#f05ce178f3201cd5268645eb14dc5bcfefa05a4c" + integrity sha512-3DfDzGTUnvG/v2U0lhBaB05j4x0bczrgylrg5Co6LMyRYh1kPA4XnK0dTshSxA6igjKqAjSacfwZQ40f4rLdqw== dependencies: "@types/uuid" "^8.3.3" uuid "^8.3.2" +"@grafana/experimental@1.7.3": + version "1.7.3" + resolved "https://registry.yarnpkg.com/@grafana/experimental/-/experimental-1.7.3.tgz#a07399fda8dc602bffe85bb57b8a7ae209a5ee92" + integrity sha512-Un9L8KNQTkTYyNKAJAr0rOG/MjjgS6/oD5eY4p3VWlKiUe89K2JNXTMH3Bbj4oMTU8EC+x8tEcAohbVjUPqIcw== + dependencies: + "@types/uuid" "^8.3.3" + semver "^7.5.4" + uuid "^8.3.2" + "@grafana/faro-core@^1.0.0-beta2": version "1.0.0-beta4" resolved "https://registry.yarnpkg.com/@grafana/faro-core/-/faro-core-1.0.0-beta4.tgz#2f38e18764c0a3c3f1af889d510a2896bcb742ab" From 95b6b42a059963d68d3c5795f207cfb2217bc656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ida=20=C5=A0tambuk?= Date: Thu, 26 Oct 2023 16:10:02 +0200 Subject: [PATCH 2/4] Fix cascader, typo --- .../QueryEditor/AccountIdDropdown.tsx | 4 +- .../QueryEditor/QueryEditor.test.tsx | 2 +- .../QueryEditor/QueryEditorForm.tsx | 95 ++++++++----------- .../QueryEditor/QueryEditorFormOld.tsx | 8 +- src/components/QueryEditor/QuerySection.tsx | 9 +- src/components/QueryEditor/constants.ts | 94 +----------------- 6 files changed, 52 insertions(+), 160 deletions(-) diff --git a/src/components/QueryEditor/AccountIdDropdown.tsx b/src/components/QueryEditor/AccountIdDropdown.tsx index 3ebb494..7bd35bd 100644 --- a/src/components/QueryEditor/AccountIdDropdown.tsx +++ b/src/components/QueryEditor/AccountIdDropdown.tsx @@ -9,7 +9,7 @@ import { EditorField } from '@grafana/experimental'; type Props = { datasource: XrayDataSource; query: XrayQuery; - newFornStylingEnabled?: boolean; + newFormStylingEnabled?: boolean; range?: TimeRange; onChange: (items: string[]) => void; }; @@ -23,7 +23,7 @@ export const AccountIdDropdown = (props: Props) => { return null; } - return props.newFornStylingEnabled ? ( + return props.newFormStylingEnabled ? ( { query: 'test query', queryType: type as XrayQueryType, }); - expect(screen.getByDisplayValue(expected)).not.toBeNull(); + expect(screen.getByText(expected)).not.toBeNull(); }); it('inits the query with query type', async () => { diff --git a/src/components/QueryEditor/QueryEditorForm.tsx b/src/components/QueryEditor/QueryEditorForm.tsx index 8212902..436df90 100644 --- a/src/components/QueryEditor/QueryEditorForm.tsx +++ b/src/components/QueryEditor/QueryEditorForm.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { css } from '@emotion/css'; import { QueryEditorProps, ScopedVars } from '@grafana/data'; -import { MultiSelect, Select, Cascader } from '@grafana/ui'; +import { MultiSelect, Select, ButtonCascader } from '@grafana/ui'; import { Group, Region, XrayJsonData, XrayQuery, XrayQueryType } from '../../types'; import { useInitQuery } from './useInitQuery'; import { @@ -21,19 +21,22 @@ import { EditorRow, EditorFieldGroup, EditorField } from '@grafana/experimental' import { QuerySection } from './QuerySection'; import { XrayLinks } from './XrayLinks'; -function findOptionForQueryType(queryType: XrayQueryType, options: any = queryTypeOptions): QueryTypeOption | null { +function findOptionForQueryType(queryType: XrayQueryType, options: any = queryTypeOptions): QueryTypeOption[] { for (const option of options) { + const selected: QueryTypeOption[] = []; if (option.queryType === queryType) { - return option; + selected.push(option); + return selected; } - if (option.items) { - const found = findOptionForQueryType(queryType, option.items); - if (found) { - return found; + if (option.children) { + const found = findOptionForQueryType(queryType, option.children); + if (found.length) { + selected.push(option, ...found); + return selected; } } } - return null; + return []; } /** @@ -41,58 +44,33 @@ function findOptionForQueryType(queryType: XrayQueryType, options: any = queryTy * between trace list and single trace and we detect that based on query. So trace list option returns single trace * if query contains single traceID. */ -export function queryTypeToQueryTypeOptions(queryType?: XrayQueryType): QueryTypeOption | null { +function queryTypeToQueryTypeOptions(queryType?: XrayQueryType): QueryTypeOption[] { if (!queryType || queryType === XrayQueryType.getTimeSeriesServiceStatistics) { - return traceStatisticsOption; + return [traceStatisticsOption]; } if (queryType === XrayQueryType.getTrace || queryType === XrayQueryType.getTraceSummaries) { - return traceListOption; + return [traceListOption]; } if (queryType === XrayQueryType.getInsights) { - return insightsOption; + return [insightsOption]; } return findOptionForQueryType(queryType); } -// recursively search for the selected option in cascade option's item or item.items -export function findQueryTypeOption(options: QueryTypeOption[], selected: string): QueryTypeOption | undefined { - for (const option of options) { - // Check if the current option's value matches the selected value - if (option.value === selected) { - return option; - } - - // If no match was found at the current level, check items if they exist - if (option.items) { - const result = findQueryTypeOption(option.items, selected); - if (result) { - return result; - } - } - } - // If no match was found in the current array or its items, return undefined - return undefined; -} - -export function queryTypeOptionToQueryType( - selected: string, - query: string, - scopedVars?: ScopedVars -): XrayQueryType | undefined { - if (selected === traceListOption.value) { +export function queryTypeOptionToQueryType(selected: string[], query: string, scopedVars?: ScopedVars): XrayQueryType { + if (selected[0] === traceListOption.value) { const resolvedQuery = getTemplateSrv().replace(query, scopedVars); const isTraceIdQuery = /^\d-\w{8}-\w{24}$/.test(resolvedQuery.trim()); return isTraceIdQuery ? XrayQueryType.getTrace : XrayQueryType.getTraceSummaries; } else { - const foundItem = findQueryTypeOption(queryTypeOptions, selected); - if (!foundItem) { - console.log('item could not be found in the options'); + let found: any = undefined; + for (const path of selected) { + found = (found?.children ?? queryTypeOptions).find((option: QueryTypeOption) => option.value === path); } - console.log(JSON.stringify(foundItem, null, 2)); - return foundItem?.queryType ?? undefined; + return found.queryType; } } @@ -128,8 +106,9 @@ export function QueryEditorForm({ const allRegions = [{ label: 'default', value: 'default', text: 'default' }, ...regions]; useInitQuery(query, onChange, groups, allRegions, datasource); - const selectedOption = queryTypeToQueryTypeOptions(query.queryType); - const allGroups = selectedOption === insightsOption ? [dummyAllGroup, ...groups] : groups; + const selectedOptions = queryTypeToQueryTypeOptions(query.queryType); + + const allGroups = selectedOptions[0] === insightsOption ? [dummyAllGroup, ...groups] : groups; const styles = getStyles(); return ( @@ -137,11 +116,11 @@ export function QueryEditorForm({ - option.value)} options={queryTypeOptions} - changeOnSelect={false} - onSelect={(value: string) => { + onChange={(value) => { const newQueryType = queryTypeOptionToQueryType(value, query.query || '', data?.request?.scopedVars); onChange({ ...query, @@ -149,7 +128,9 @@ export function QueryEditorForm({ columns: newQueryType === XrayQueryType.getTimeSeriesServiceStatistics ? ['all'] : undefined, } as any); }} - /> + > + {selectedOptions[selectedOptions.length - 1].label} + )} - {selectedOption === traceStatisticsOption && ( + {selectedOptions[0] === traceStatisticsOption && ( - {selectedOption && ![insightsOption, serviceMapOption].includes(selectedOption) && ( + {![insightsOption, serviceMapOption].includes(selectedOptions[0]) && ( )} - {selectedOption === traceStatisticsOption && ( + {selectedOptions[0] === traceStatisticsOption && ( option.value === path); + found = (found?.children ?? queryTypeOptions).find((option: QueryTypeOption) => option.value === path); } return found.queryType; } @@ -146,7 +146,7 @@ export function QueryEditorFormOld({ option.value)} - options={queryTypeOptionsOld} + options={queryTypeOptions} onChange={(value) => { const newQueryType = queryTypeOptionToQueryType(value, query.query || '', data?.request?.scopedVars); onChange({ diff --git a/src/components/QueryEditor/QuerySection.tsx b/src/components/QueryEditor/QuerySection.tsx index de9dc0f..c265285 100644 --- a/src/components/QueryEditor/QuerySection.tsx +++ b/src/components/QueryEditor/QuerySection.tsx @@ -22,10 +22,10 @@ type Props = { datasource: XrayDataSource; onChange: (value: XrayQuery) => void; onRunQuery: () => void; - selectedOption: QueryTypeOption; + selectedOptions: QueryTypeOption[]; }; export function QuerySection(props: Props) { - const { datasource, query, onRunQuery, onChange, selectedOption } = props; + const { datasource, query, onRunQuery, onChange, selectedOptions } = props; const onRunQueryLocal = () => { onChange(query); @@ -63,7 +63,10 @@ export function QuerySection(props: Props) { onChange={(e) => { onChange({ ...query, - queryType: queryTypeOptionToQueryType(selectedOption.value, e.query), + queryType: queryTypeOptionToQueryType( + selectedOptions.map((option) => option.value), + e.query + ), query: e.query, }); }} diff --git a/src/components/QueryEditor/constants.ts b/src/components/QueryEditor/constants.ts index 30f7577..476b505 100644 --- a/src/components/QueryEditor/constants.ts +++ b/src/components/QueryEditor/constants.ts @@ -25,7 +25,7 @@ export const traceStatisticsOption: QueryTypeOption = { queryType: XrayQueryType.getTimeSeriesServiceStatistics, }; -export const queryTypeOptionsOld: QueryTypeOption[] = [ +export const queryTypeOptions: QueryTypeOption[] = [ traceListOption, traceStatisticsOption, insightsOption, @@ -117,98 +117,6 @@ export const queryTypeOptionsOld: QueryTypeOption[] = [ serviceMapOption, ]; -export const queryTypeOptions: QueryTypeOption[] = [ - traceListOption, - traceStatisticsOption, - insightsOption, - { - label: 'Trace Analytics', - value: 'traceAnalytics', - items: [ - { - value: 'rootCause', - label: 'Root Cause', - items: [ - { - value: 'responseTime', - label: 'Response Time', - items: [ - { - value: 'rootCauseService', - label: 'Root Cause', - queryType: XrayQueryType.getAnalyticsRootCauseResponseTimeService, - } as QueryTypeOption, - { - value: 'path', - label: 'Path', - queryType: XrayQueryType.getAnalyticsRootCauseResponseTimePath, - }, - ], - }, - { - value: 'error', - label: 'Error', - items: [ - { - value: 'rootCauseService', - label: 'Root Cause', - queryType: XrayQueryType.getAnalyticsRootCauseErrorService, - }, - { - value: 'path', - label: 'Path', - queryType: XrayQueryType.getAnalyticsRootCauseErrorPath, - }, - { - value: 'message', - label: 'Error Message', - queryType: XrayQueryType.getAnalyticsRootCauseErrorMessage, - }, - ], - }, - { - value: 'fault', - label: 'Fault', - items: [ - { - value: 'rootCauseService', - label: 'Root Cause', - queryType: XrayQueryType.getAnalyticsRootCauseFaultService, - }, - { - value: 'path', - label: 'Path', - queryType: XrayQueryType.getAnalyticsRootCauseFaultPath, - }, - { - value: 'message', - label: 'Error Message', - queryType: XrayQueryType.getAnalyticsRootCauseFaultMessage, - }, - ], - }, - ], - }, - { - value: 'user', - label: 'End user impact', - queryType: XrayQueryType.getAnalyticsUser, - } as QueryTypeOption, - { - value: 'url', - label: 'URL', - queryType: XrayQueryType.getAnalyticsUrl, - }, - { - value: 'statusCode', - label: 'HTTP status code', - queryType: XrayQueryType.getAnalyticsStatusCode, - }, - ], - }, - serviceMapOption, -]; - export const columnNames: { [key: string]: string } = { 'ErrorStatistics.ThrottleCount': 'Throttle Count', 'ErrorStatistics.TotalCount': 'Error Count', From f559d36b6f4cca11c6667aafbfa9f48fea11457f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ida=20=C5=A0tambuk?= Date: Fri, 27 Oct 2023 15:53:20 +0200 Subject: [PATCH 3/4] Improve tests --- src/components/ConfigEditor/ConfigEditor.tsx | 1 - .../QueryEditor/QueryEditor.test.tsx | 322 +++++++++--------- 2 files changed, 170 insertions(+), 153 deletions(-) diff --git a/src/components/ConfigEditor/ConfigEditor.tsx b/src/components/ConfigEditor/ConfigEditor.tsx index 581fb08..cea1278 100644 --- a/src/components/ConfigEditor/ConfigEditor.tsx +++ b/src/components/ConfigEditor/ConfigEditor.tsx @@ -12,7 +12,6 @@ export function ConfigEditor(props: Props) { return (
- {/* can add x-ray specific things here */}
); } diff --git a/src/components/QueryEditor/QueryEditor.test.tsx b/src/components/QueryEditor/QueryEditor.test.tsx index 91e7860..c747445 100644 --- a/src/components/QueryEditor/QueryEditor.test.tsx +++ b/src/components/QueryEditor/QueryEditor.test.tsx @@ -27,6 +27,12 @@ const defaultProps = { } as any, }; +const originalFormFeatureToggleValue = grafanaRuntime.config.featureToggles.awsDatasourcesNewFormStyling; + +const cleanupFeatureToggle = () => { + grafanaRuntime.config.featureToggles.awsDatasourcesNewFormStyling = originalFormFeatureToggleValue; +}; + jest.mock('./XRayQueryField', () => { return { __esModule: true, @@ -106,185 +112,197 @@ async function renderWithQuery(query: Omit, rerender?: any) } describe('QueryEditor', () => { - function run(testName: string) { - describe(testName, () => { - it.each([ - [XrayQueryType.getTrace, 'Trace List'], - [XrayQueryType.getTraceSummaries, 'Trace List'], - [XrayQueryType.getTimeSeriesServiceStatistics, 'Trace Statistics'], - [XrayQueryType.getAnalyticsRootCauseResponseTimeService, 'Root Cause'], - [XrayQueryType.getAnalyticsRootCauseResponseTimePath, 'Path'], - [XrayQueryType.getAnalyticsRootCauseErrorService, 'Root Cause'], - [XrayQueryType.getAnalyticsRootCauseErrorPath, 'Path'], - [XrayQueryType.getAnalyticsRootCauseErrorMessage, 'Error Message'], - [XrayQueryType.getAnalyticsRootCauseFaultService, 'Root Cause'], - [XrayQueryType.getAnalyticsRootCauseFaultPath, 'Path'], - [XrayQueryType.getAnalyticsRootCauseFaultMessage, 'Error Message'], - [XrayQueryType.getAnalyticsUser, 'End user impact'], - [XrayQueryType.getAnalyticsUrl, 'URL'], - [XrayQueryType.getAnalyticsStatusCode, 'HTTP status code'], - [XrayQueryType.getInsights, 'Insights'], - [XrayQueryType.getServiceMap, 'Service Map'], - ])('renders proper query type option when query type is %s', async (type, expected) => { - await renderWithQuery({ - query: 'test query', - queryType: type as XrayQueryType, - }); - expect(screen.getByText(expected)).not.toBeNull(); + function run() { + it.each([ + [XrayQueryType.getTrace, 'Trace List'], + [XrayQueryType.getTraceSummaries, 'Trace List'], + [XrayQueryType.getTimeSeriesServiceStatistics, 'Trace Statistics'], + [XrayQueryType.getAnalyticsRootCauseResponseTimeService, 'Root Cause'], + [XrayQueryType.getAnalyticsRootCauseResponseTimePath, 'Path'], + [XrayQueryType.getAnalyticsRootCauseErrorService, 'Root Cause'], + [XrayQueryType.getAnalyticsRootCauseErrorPath, 'Path'], + [XrayQueryType.getAnalyticsRootCauseErrorMessage, 'Error Message'], + [XrayQueryType.getAnalyticsRootCauseFaultService, 'Root Cause'], + [XrayQueryType.getAnalyticsRootCauseFaultPath, 'Path'], + [XrayQueryType.getAnalyticsRootCauseFaultMessage, 'Error Message'], + [XrayQueryType.getAnalyticsUser, 'End user impact'], + [XrayQueryType.getAnalyticsUrl, 'URL'], + [XrayQueryType.getAnalyticsStatusCode, 'HTTP status code'], + [XrayQueryType.getInsights, 'Insights'], + [XrayQueryType.getServiceMap, 'Service Map'], + ])('renders proper query type option when query type is %s', async (type, expected) => { + await renderWithQuery({ + query: 'test query', + queryType: type as XrayQueryType, }); + expect(screen.getByText(expected)).not.toBeNull(); + }); - it('inits the query with query type', async () => { - const { onChange } = await renderWithQuery({ query: '' }); - expect(onChange).toBeCalledWith({ - refId: 'A', - query: '', - queryType: XrayQueryType.getTraceSummaries, - region: 'default', - group: { GroupName: 'Default', GroupARN: 'DefaultARN' }, - }); + it('inits the query with query type', async () => { + const { onChange } = await renderWithQuery({ query: '' }); + expect(onChange).toBeCalledWith({ + refId: 'A', + query: '', + queryType: XrayQueryType.getTraceSummaries, + region: 'default', + group: { GroupName: 'Default', GroupARN: 'DefaultARN' }, }); + }); - it('shows column filter and resolution only if query type is getTimeSeriesServiceStatistics', async () => { - const { rerender } = await renderWithQuery({ query: '', queryType: XrayQueryType.getTraceSummaries }); - expect(screen.queryByTestId('column-filter')).toBeNull(); - expect(screen.queryByTestId('resolution')).toBeNull(); + it('shows column filter and resolution only if query type is getTimeSeriesServiceStatistics', async () => { + const { rerender } = await renderWithQuery({ query: '', queryType: XrayQueryType.getTraceSummaries }); + expect(screen.queryByTestId('column-filter')).toBeNull(); + expect(screen.queryByTestId('resolution')).toBeNull(); - await renderWithQuery({ query: '', queryType: XrayQueryType.getTimeSeriesServiceStatistics }, rerender); - expect(screen.queryByTestId('column-filter')).not.toBeNull(); - expect(screen.queryByTestId('resolution')).not.toBeNull(); - }); + await renderWithQuery({ query: '', queryType: XrayQueryType.getTimeSeriesServiceStatistics }, rerender); + expect(screen.queryByTestId('column-filter')).not.toBeNull(); + expect(screen.queryByTestId('resolution')).not.toBeNull(); + }); - it('hides query input if query is service map', async () => { - await renderWithQuery({ query: '', queryType: XrayQueryType.getServiceMap }); - expect(screen.queryByText(/^Query$/)).toBeNull(); - }); + it('hides query input if query is service map', async () => { + await renderWithQuery({ query: '', queryType: XrayQueryType.getServiceMap }); + expect(screen.queryByText(/^Query$/)).toBeNull(); + }); - it('correctly changes the query type if user fills in trace id', async () => { - const { onChange } = await renderWithQuery({ query: '', queryType: XrayQueryType.getTraceSummaries }); + it('correctly changes the query type if user fills in trace id', async () => { + const { onChange } = await renderWithQuery({ query: '', queryType: XrayQueryType.getTraceSummaries }); - const field = screen.getByTestId('query-field-mock'); + const field = screen.getByTestId('query-field-mock'); - fireEvent.change(field, { target: { value: '1-5f160a8b-83190adad07f429219c0e259' } }); + fireEvent.change(field, { target: { value: '1-5f160a8b-83190adad07f429219c0e259' } }); - expect(onChange.mock.calls[1][0]).toEqual({ - refId: 'A', - query: '1-5f160a8b-83190adad07f429219c0e259', - queryType: XrayQueryType.getTrace, - }); + expect(onChange.mock.calls[1][0]).toEqual({ + refId: 'A', + query: '1-5f160a8b-83190adad07f429219c0e259', + queryType: XrayQueryType.getTrace, }); + }); - it('can add and remove column filters', async () => { - let { onChange } = await renderWithQuery({ - query: '', - columns: [], - queryType: XrayQueryType.getTimeSeriesServiceStatistics, - }); + it('can add and remove column filters', async () => { + let { onChange } = await renderWithQuery({ + query: '', + columns: [], + queryType: XrayQueryType.getTimeSeriesServiceStatistics, + }); - let select = screen.getByText('All columns'); - fireEvent.mouseDown(select); - let option = screen.getByText(/Success Count/i); - fireEvent.click(option); + let select = screen.getByText('All columns'); + fireEvent.mouseDown(select); + let option = screen.getByText(/Success Count/i); + fireEvent.click(option); - expect(onChange).toBeCalledWith({ - refId: 'A', - query: '', - columns: ['OkCount'], - queryType: XrayQueryType.getTimeSeriesServiceStatistics, - }); + expect(onChange).toBeCalledWith({ + refId: 'A', + query: '', + columns: ['OkCount'], + queryType: XrayQueryType.getTimeSeriesServiceStatistics, }); + }); - it('waits until groups and regions are loaded', async () => { - await act(async () => { - render( - {}} - /> - ); - // No ideal selector but spinner does not seem to have any better thing to select by - expect(screen.getByText('', { selector: '.fa-spinner' })).toBeDefined(); - await waitFor(() => expect(screen.getByText('Query')).toBeDefined()); - }); + it('waits until groups and regions are loaded', async () => { + await act(async () => { + render( + {}} + /> + ); + // No ideal selector but spinner does not seem to have any better thing to select by + expect(screen.getByText('', { selector: '.fa-spinner' })).toBeDefined(); + await waitFor(() => expect(screen.getByText('Query')).toBeDefined()); }); + }); - it('sets the correct links based on region default', async () => { - renderEditorWithRegion('region1', 'default'); - await checkLinks({ - console: 'https://region1.console.aws.amazon.com/xray/home?region=region1#/analytics', - serviceMap: 'https://region1.console.aws.amazon.com/xray/home?region=region1#/service-map/', - }); + it('sets the correct links based on region default', async () => { + renderEditorWithRegion('region1', 'default'); + await checkLinks({ + console: 'https://region1.console.aws.amazon.com/xray/home?region=region1#/analytics', + serviceMap: 'https://region1.console.aws.amazon.com/xray/home?region=region1#/service-map/', }); + }); - it('sets the correct links based on region in query', async () => { - renderEditorWithRegion('region1', 'region2'); - await checkLinks({ - console: 'https://region2.console.aws.amazon.com/xray/home?region=region2#/analytics', - serviceMap: 'https://region2.console.aws.amazon.com/xray/home?region=region2#/service-map/', - }); + it('sets the correct links based on region in query', async () => { + renderEditorWithRegion('region1', 'region2'); + await checkLinks({ + console: 'https://region2.console.aws.amazon.com/xray/home?region=region2#/analytics', + serviceMap: 'https://region2.console.aws.amazon.com/xray/home?region=region2#/service-map/', }); + }); - it('shows the accountIds in a dropdown on service map selection', async () => { - const mockGetAccountIdsForServiceMap = jest.fn(() => Promise.resolve(['account1', 'account2'])); - await act(async () => { - render( - {}} - /> - ); - expect(screen.getByText('', { selector: '.fa-spinner' })).toBeDefined(); - await waitFor(() => expect(screen.getByText('account1')).toBeDefined()); - expect(mockGetAccountIdsForServiceMap).toHaveBeenCalled(); - }); + it('shows the accountIds in a dropdown on service map selection', async () => { + const mockGetAccountIdsForServiceMap = jest.fn(() => Promise.resolve(['account1', 'account2'])); + await act(async () => { + render( + {}} + /> + ); + expect(screen.getByText('', { selector: '.fa-spinner' })).toBeDefined(); + await waitFor(() => expect(screen.getByText('account1')).toBeDefined()); + expect(mockGetAccountIdsForServiceMap).toHaveBeenCalled(); }); + }); - it('does not fetch account ids if service map is not selected', async () => { - const mockGetAccountIdsForServiceMap = jest.fn(() => Promise.resolve(['account1', 'account2'])); - await act(async () => { - render( - {}} - /> - ); - expect(screen.getByText('', { selector: '.fa-spinner' })).toBeDefined(); - await waitForElementToBeRemoved(() => screen.getByText('', { selector: '.fa-spinner' })); - expect(mockGetAccountIdsForServiceMap).not.toHaveBeenCalled(); - }); + it('does not fetch account ids if service map is not selected', async () => { + const mockGetAccountIdsForServiceMap = jest.fn(() => Promise.resolve(['account1', 'account2'])); + await act(async () => { + render( + {}} + /> + ); + expect(screen.getByText('', { selector: '.fa-spinner' })).toBeDefined(); + await waitForElementToBeRemoved(() => screen.getByText('', { selector: '.fa-spinner' })); + expect(mockGetAccountIdsForServiceMap).not.toHaveBeenCalled(); }); }); } - - run('QueryEditorForm with awsDatasourcesNewFormStyling disabled'); - grafanaRuntime.config.featureToggles.awsDatasourcesNewFormStyling = true; - run('QueryEditorForm with awsDatasourcesNewFormStyling enabled'); + describe('QueryEditor with awsDatasourcesNewFormStyling feature toggle enabled', () => { + beforeAll(() => { + grafanaRuntime.config.featureToggles.awsDatasourcesNewFormStyling = false; + }); + afterAll(() => { + cleanupFeatureToggle(); + }); + run(); + describe('QueryEditor with awsDatasourcesNewFormStyling feature toggle enabled', () => { + beforeAll(() => { + grafanaRuntime.config.featureToggles.awsDatasourcesNewFormStyling = true; + }); + afterAll(() => { + cleanupFeatureToggle(); + }); + run(); + }); + }); }); function makeDataSource(settings: DataSourceInstanceSettings) { From bd83427a73487065271b26dcb8856dea44dfb516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ida=20=C5=A0tambuk?= Date: Tue, 31 Oct 2023 12:06:25 +0100 Subject: [PATCH 4/4] Prepare 2.8.0 --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 761c9e4..a51bbf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. +## 2.8.0 + +- Migrate ConfigEditor and QueryEditor to the new form styling [#211](https://github.com/grafana/x-ray-datasource/pull/211) + +- Bump google.golang.org/grpc from 1.54.0 to 1.56.3 in [#210](https://github.com/grafana/x-ray-datasource/pull/210) + +- Support Node 18 in [201](https://github.com/grafana/x-ray-datasource/pull/201) + ## 2.7.2 - Fix X-Ray Service Map filter trace list query by @jamesrwhite in https://github.com/grafana/x-ray-datasource/pull/203 diff --git a/package.json b/package.json index d5b60cb..0714eda 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "grafana-x-ray-datasource", - "version": "2.7.2", + "version": "2.8.0", "description": "AWS X-Ray data source", "scripts": { "build": "webpack -c ./.config/webpack/webpack.config.ts --env production",