diff --git a/.config/bundler/externals.ts b/.config/bundler/externals.ts new file mode 100644 index 0000000..44f4556 --- /dev/null +++ b/.config/bundler/externals.ts @@ -0,0 +1,50 @@ +/* + * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ + * + */ + +import type { Configuration, ExternalItemFunctionData } from 'webpack'; + +type ExternalsType = Configuration['externals']; + +export const externals: ExternalsType = [ + // Required for dynamic publicPath resolution + { 'amd-module': 'module' }, + 'lodash', + 'jquery', + 'moment', + 'slate', + 'emotion', + '@emotion/react', + '@emotion/css', + 'prismjs', + 'slate-plain-serializer', + '@grafana/slate-react', + 'react', + 'react/jsx-runtime', + 'react/jsx-dev-runtime', + 'react-dom', + 'react-redux', + 'redux', + 'rxjs', + 'i18next', + 'react-router', + 'd3', + 'angular', + /^@grafana\/ui/i, + /^@grafana\/runtime/i, + /^@grafana\/data/i, + + // Mark legacy SDK imports as external if their name starts with the "grafana/" prefix + ({ request }: ExternalItemFunctionData, callback: (error?: Error, result?: string) => void) => { + const prefix = 'grafana/'; + const hasPrefix = (request: string) => request.indexOf(prefix) === 0; + const stripPrefix = (request: string) => request.slice(prefix.length); + + if (request && hasPrefix(request)) { + return callback(undefined, stripPrefix(request)); + } + + callback(); + }, +]; diff --git a/.config/webpack/webpack.config.ts b/.config/webpack/webpack.config.ts index 3b21b72..0707178 100644 --- a/.config/webpack/webpack.config.ts +++ b/.config/webpack/webpack.config.ts @@ -16,6 +16,8 @@ import { Configuration } from 'webpack'; import { getPackageJson, getPluginJson, hasReadme, getEntries, isWSL } from './utils'; import { SOURCE_DIR, DIST_DIR } from './constants'; +import { externals } from '../bundler/externals.ts'; + const pluginJson = getPluginJson(); const config = async (env): Promise => { @@ -33,43 +35,7 @@ const config = async (env): Promise => { entry: await getEntries(), - externals: [ - 'lodash', - 'jquery', - 'moment', - 'slate', - 'emotion', - '@emotion/react', - '@emotion/css', - 'prismjs', - 'slate-plain-serializer', - '@grafana/slate-react', - 'react', - 'react-dom', - 'react-redux', - 'redux', - 'rxjs', - 'react-router', - 'react-router-dom', - 'd3', - 'angular', - '@grafana/ui', - '@grafana/runtime', - '@grafana/data', - - // Mark legacy SDK imports as external if their name starts with the "grafana/" prefix - ({ request }, callback) => { - const prefix = 'grafana/'; - const hasPrefix = (request) => request.indexOf(prefix) === 0; - const stripPrefix = (request) => request.substr(prefix.length); - - if (hasPrefix(request)) { - return callback(undefined, stripPrefix(request)); - } - - callback(); - }, - ], + externals, mode: env.production ? 'production' : 'development', diff --git a/QA_CHECKLIST.md b/QA_CHECKLIST.md new file mode 100644 index 0000000..e302ccb --- /dev/null +++ b/QA_CHECKLIST.md @@ -0,0 +1,56 @@ +# QA Checklist — Quickwit Grafana Datasource Plugin + +## Plugin-owned UI areas + +### Config Editor (`/connections/datasources/edit/`) + +- [ ] **Index settings section** + - [ ] Index ID field accepts input and saves + - [ ] Message field name field accepts input and saves + - [ ] Log level field accepts input and saves +- [ ] **Editor settings section** + - [ ] Default logs limit field accepts input and saves +- [ ] **Data links section** + - [ ] Can add a data link + - [ ] Can remove a data link +- [ ] **Save & test** validates connection to Quickwit and reports success/failure + +### Query Editor — Explore view (`/explore`) + +- [ ] **Query type tabs** + - [ ] Metrics tab loads metric aggregation options + - [ ] Logs tab loads log query options + - [ ] Raw Data tab loads raw query options +- [ ] **Lucene Query input** + - [ ] Can type a query + - [ ] Autocomplete works (Ctrl+Space) + - [ ] Shift+Enter runs the query +- [ ] **Logs config row** + - [ ] Tail count is configurable + - [ ] Sort direction is configurable +- [ ] **Results** + - [ ] Logs volume histogram renders with data + - [ ] Log rows render in the Logs panel + - [ ] Table view toggle works + - [ ] "Scan for older logs" button works + +### Query Editor — Dashboard panels + +- [ ] **Metrics query type** + - [ ] Date histogram aggregation works + - [ ] Terms aggregation works + - [ ] Metric aggregations (avg, sum, count, etc.) work +- [ ] **Logs query type** renders logs in panel +- [ ] **Ad-hoc filters** can be added and filter results dynamically + +## Not owned by the plugin (Grafana built-in) + +These are rendered by Grafana itself and do not need plugin-level QA: + +- Sidebar navigation +- Time picker +- Log row rendering / expansion +- Chart rendering (histogram, time series) +- Search bar, breadcrumbs, header +- Auth section in config (Basic auth, TLS, OAuth) +- HTTP section in config (URL, Allowed cookies, Timeout) diff --git a/docker-compose.yaml b/docker-compose.yaml index 44d1326..fac0e92 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -16,7 +16,8 @@ services: build: context: ./.config args: - grafana_version: 12.1.0 + grafana_version: ${GRAFANA_VERSION:-12.1.0} + grafana_image: ${GRAFANA_IMAGE:-grafana-oss} ports: - 3000:3000/tcp volumes: diff --git a/src/components/QueryEditor/ElasticsearchQueryContext.tsx b/src/components/QueryEditor/ElasticsearchQueryContext.tsx index 919357d..7595f75 100644 --- a/src/components/QueryEditor/ElasticsearchQueryContext.tsx +++ b/src/components/QueryEditor/ElasticsearchQueryContext.tsx @@ -50,7 +50,7 @@ export const ElasticsearchProvider = withStore(({ app, datasource, range, -}: PropsWithChildren): JSX.Element => { +}: PropsWithChildren): React.JSX.Element => { const storeDispatch = useDispatch(); useEffect(()=>{ diff --git a/src/dependencies/DataSourcePicker.tsx b/src/dependencies/DataSourcePicker.tsx deleted file mode 100644 index dee889d..0000000 --- a/src/dependencies/DataSourcePicker.tsx +++ /dev/null @@ -1,159 +0,0 @@ -// Libraries -import React, { PureComponent } from 'react'; - -// Components -import { HorizontalGroup, Select } from '@grafana/ui'; -import { SelectableValue, DataSourceInstanceSettings } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors'; -import { isUnsignedPluginSignature, PluginSignatureBadge } from './PluginSignatureBadge'; -import { getDataSourceSrv } from '@grafana/runtime'; - -type OldDataSourceInstanceSettings = Omit; - -export interface Props { - onChange: (ds: OldDataSourceInstanceSettings) => void; - current: string | null; - hideTextValue?: boolean; - onBlur?: () => void; - autoFocus?: boolean; - openMenuOnFocus?: boolean; - placeholder?: string; - tracing?: boolean; - mixed?: boolean; - dashboard?: boolean; - metrics?: boolean; - annotations?: boolean; - variables?: boolean; - pluginId?: string; - noDefault?: boolean; -} - -export interface State { - error?: string; -} - -export class DataSourcePicker extends PureComponent { - dataSourceSrv = getDataSourceSrv(); - - static defaultProps: Partial = { - autoFocus: false, - openMenuOnFocus: false, - placeholder: 'Select datasource', - }; - - state: State = {}; - - constructor(props: Props) { - super(props); - } - - componentDidMount() { - const { current } = this.props; - // @ts-ignore - const dsSettings = this.dataSourceSrv.getInstanceSettings(current); - if (!dsSettings) { - this.setState({ error: 'Could not find data source ' + current }); - } - } - - onChange = (item: SelectableValue) => { - // @ts-ignore - const dsSettings = this.dataSourceSrv.getInstanceSettings(item.value); - - if (dsSettings) { - this.props.onChange(dsSettings); - this.setState({ error: undefined }); - } - }; - - private getCurrentValue() { - const { current, hideTextValue, noDefault } = this.props; - - if (!current && noDefault) { - return null; - } - - // @ts-ignore - const ds = this.dataSourceSrv.getInstanceSettings(current); - - if (ds) { - return { - label: ds.name.substr(0, 37), - value: ds.name, - imgUrl: ds.meta.info.logos.small, - hideText: hideTextValue, - meta: ds.meta, - }; - } - - return { - label: (current ?? 'no name') + ' - not found', - value: current, - imgUrl: '', - hideText: hideTextValue, - }; - } - - getDataSourceOptions() { - const { tracing, metrics, mixed, dashboard, variables, annotations, pluginId } = this.props; - const options = this.dataSourceSrv - // @ts-ignore - .getList({ - tracing, - metrics, - dashboard, - mixed, - variables, - annotations, - pluginId, - }) - .map((ds) => ({ - value: ds.name, - label: `${ds.name}${ds.isDefault ? ' (default)' : ''}`, - imgUrl: ds.meta.info.logos.small, - meta: ds.meta, - })); - - return options; - } - - render() { - const { autoFocus, onBlur, openMenuOnFocus, placeholder } = this.props; - const { error } = this.state; - const options = this.getDataSourceOptions(); - const value = this.getCurrentValue(); - - return ( -
-