Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 11 additions & 27 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ on:
jobs:
release:
runs-on: ubuntu-latest
env:
GRAFANA_API_KEY: ${{ secrets.GRAFANA_API_KEY }} # Requires a Grafana API key from Grafana.com.
# env:
# GRAFANA_API_KEY: ${{ secrets.GRAFANA_API_KEY }} # Requires a Grafana API key from Grafana.com.
steps:
- uses: actions/checkout@v3
- name: Setup Node.js environment
Expand Down Expand Up @@ -51,15 +51,15 @@ jobs:
version: latest
args: buildAll

- name: Warn missing Grafana API key
run: |
echo Please generate a Grafana API key: https://grafana.com/docs/grafana/latest/developers/plugins/sign-a-plugin/#generate-an-api-key
echo Once done please follow the instructions found here: https://github.com/${{github.repository}}/blob/main/README.md#using-github-actions-release-workflow
if: ${{ env.GRAFANA_API_KEY == '' }}

- name: Sign plugin
run: npm run sign
if: ${{ env.GRAFANA_API_KEY != '' }}
# - name: Warn missing Grafana API key
# run: |
# echo Please generate a Grafana API key: https://grafana.com/docs/grafana/latest/developers/plugins/sign-a-plugin/#generate-an-api-key
# echo Once done please follow the instructions found here: https://github.com/${{github.repository}}/blob/main/README.md#using-github-actions-release-workflow
# if: ${{ env.GRAFANA_API_KEY == '' }}
#
# - name: Sign plugin
# run: npm run sign
# if: ${{ env.GRAFANA_API_KEY != '' }}

- name: Get plugin metadata
id: metadata
Expand Down Expand Up @@ -113,19 +113,3 @@ jobs:
files: |
./${{ steps.metadata.outputs.archive }}
./${{ steps.metadata.outputs.archive-checksum }}
body: |
**This Github draft release has been created for your plugin.**

_Note: if this is the first release for your plugin please consult the [distributing-your-plugin section](https://github.com/${{github.repository}}/blob/main/README.md#distributing-your-plugin) of the README_

If you would like to submit this release to Grafana please consider the following steps:

- Check the Validate plugin step in the [release workflow](https://github.com/${{github.repository}}/commit/${{github.sha}}/checks/${{github.run_id}}) for any warnings that need attention
- Navigate to https://grafana.com/auth/sign-in/ to sign into your account
- Once logged in click **My Plugins** in the admin navigation
- Click the **Submit Plugin** button
- Fill in the Plugin Submission form:
- Paste this [.zip asset link](https://github.com/${{ github.repository }}/releases/download/v${{ steps.metadata.outputs.plugin-version }}/${{ steps.metadata.outputs.archive }}) in the Plugin URL field
- Paste this [.zip.md5 link](https://github.com/${{ github.repository }}/releases/download/v${{ steps.metadata.outputs.plugin-version }}/${{ steps.metadata.outputs.archive-checksum }}) in the MD5 field

Once done please remove these instructions and publish this release.
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
16
20
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "quickwit-datasource",
"version": "0.5.0",
"version": "0.8.2+paypay",
"description": "Quickwit datasource",
"scripts": {
"build": "webpack -c ./.config/webpack/webpack.config.ts --env production",
Expand Down
2 changes: 1 addition & 1 deletion pkg/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func main() {
// from Grafana to create different instances of SampleDatasource (per datasource
// ID). When datasource configuration changed Dispose method will be called and
// new datasource instance created using NewSampleDatasource factory.
if err := datasource.Manage("quickwit-quickwit-datasource", quickwit.NewQuickwitDatasource, datasource.ManageOpts{}); err != nil {
if err := datasource.Manage("paypay-quickwit-datasource", quickwit.NewQuickwitDatasource, datasource.ManageOpts{}); err != nil {
log.DefaultLogger.Error(err.Error())
os.Exit(1)
}
Expand Down
6 changes: 3 additions & 3 deletions src/components/LuceneQueryEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useRef, useCallback } from "react";
import { css } from "@emotion/css";


import CodeMirror, { ReactCodeMirrorRef } from '@uiw/react-codemirror';
import CodeMirror, { Prec, ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { keymap } from '@codemirror/view';
import { linter, Diagnostic, lintGutter } from "@codemirror/lint"
import { autocompletion, CompletionContext, startCompletion } from "@codemirror/autocomplete"
Expand Down Expand Up @@ -58,7 +58,7 @@ export function LuceneQueryEditor(props: LuceneQueryEditorProps){
maxRenderedOptions: 30,
})

const myKeymap = keymap.of([
const myKeymap = Prec.highest(keymap.of([
{
key: 'Shift-Enter',
run: (view) => {
Expand All @@ -72,7 +72,7 @@ export function LuceneQueryEditor(props: LuceneQueryEditorProps){
return startCompletion(view);
}
},
]);
]));

return (<CodeMirror
ref={editorRef}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe('Terms Settings Editor', () => {
query: '',
bucketAggs: [termsAgg],
metrics: [avg, derivative, topMetrics],
filters: [],
};

renderWithESProvider(<TermsSettingsEditor bucketAgg={termsAgg} />, { providerProps: { query } });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const query: ElasticsearchQuery = {
query: '',
metrics: [{ id: '1', type: 'count' }],
bucketAggs: [{ type: 'date_histogram', id: '2' }],
filters: []
};

describe('ElasticsearchQueryContext', () => {
Expand Down
6 changes: 4 additions & 2 deletions src/components/QueryEditor/ElasticsearchQueryContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ElasticsearchQuery } from '@/types';

import { createReducer as createBucketAggsReducer } from './BucketAggregationsEditor/state/reducer';
import { reducer as metricsReducer } from './MetricAggregationsEditor/state/reducer';
import { reducer as filtersReducer } from './FilterEditor/state/reducer';
import { aliasPatternReducer, queryReducer, initQuery, initExploreQuery } from './state';
import { getHook } from '@/utils/context';
import { Provider, useDispatch } from "react-redux";
Expand Down Expand Up @@ -64,10 +65,11 @@ export const ElasticsearchProvider = withStore(({
[onChange]
);

const reducer = combineReducers<Pick<ElasticsearchQuery, 'query' | 'alias' | 'metrics' | 'bucketAggs'>>({
const reducer = combineReducers<Pick<ElasticsearchQuery, 'query' | 'alias' | 'metrics' | 'filters' | 'bucketAggs'>>({
query: queryReducer,
alias: aliasPatternReducer,
metrics: metricsReducer,
filters: filtersReducer,
bucketAggs: createBucketAggsReducer(datasource.timeField),
});

Expand All @@ -78,7 +80,7 @@ export const ElasticsearchProvider = withStore(({
reducer
);

const isUninitialized = !query.metrics || !query.bucketAggs || query.query === undefined;
const isUninitialized = !query.metrics || !query.filters || !query.bucketAggs || query.query === undefined;

const [shouldRunInit, setShouldRunInit] = useState(isUninitialized);

Expand Down
162 changes: 162 additions & 0 deletions src/components/QueryEditor/FilterEditor/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import React, { useRef } from 'react';

import { useDispatch } from '@/hooks/useStatelessReducer';
import { IconButton } from '../../IconButton';
import { useQuery } from '../ElasticsearchQueryContext';
import { QueryEditorRow } from '../QueryEditorRow';

import { QueryFilter } from '@/types';
import { InlineSegmentGroup, Input, Segment, SegmentAsync, Tooltip } from '@grafana/ui';
import {
addFilter,
removeFilter,
toggleFilterVisibility,
changeFilterField,
changeFilterOperation,
changeFilterValue,
} from '@/components/QueryEditor/FilterEditor/state/actions';
import { segmentStyles } from '@/components/QueryEditor/styles';
import { useFields } from '@/hooks/useFields';
import { newFilterId } from '@/utils/uid';
import { filterOperations } from '@/queryDef';
import { hasWhiteSpace, isSet } from '@/utils';

interface FilterEditorProps {
onSubmit: () => void;
}

function filterErrors(filter: QueryFilter): string[] {
const errors: string[] = [];

if (!isSet(filter.filter.key)) {
errors.push('Field is not set');
}

if (!isSet(filter.filter.operator)) {
errors.push('Operator is not set');
}

if (!['exists', 'not exists'].includes(filter.filter.operator) && !isSet(filter.filter.value)) {
errors.push('Value is not set');
}

if (['term', 'not term'].includes(filter.filter.operator) && filter.filter.value && hasWhiteSpace(filter.filter.value)) {
errors.push('Term cannot have whitespace in value');
}

return errors;
}

export const FilterEditor = ({ onSubmit }: FilterEditorProps) => {
const dispatch = useDispatch();
const { filters } = useQuery();

return (
<>
{filters?.map((filter, index) => {
const errors = filterErrors(filter)
return (
<QueryEditorRow
key={`${filter.id}`}
label={errors.length > 0 ? (
<Tooltip content={errors.join('; ')}>
<span style={{color: "gray"}}>Filter</span>
</Tooltip>
): 'Filter'}
hidden={filter.hide}
onHideClick={() => {
dispatch(toggleFilterVisibility(filter.id));
onSubmit();
}}
onRemoveClick={() => {
dispatch(removeFilter(filter.id));
onSubmit();
}}
>
<FilterEditorRow value={filter} onSubmit={onSubmit} />

{index === 0 && <IconButton
label="add"
iconName="plus"
style={{marginLeft: '4px'}}
onClick={() => dispatch(addFilter(newFilterId()))}
/>}
</QueryEditorRow>
)
})}
</>
);
};

interface FilterEditorRowProps {
value: QueryFilter;
onSubmit: () => void;
}

export const FilterEditorRow = ({ value, onSubmit }: FilterEditorRowProps) => {
const dispatch = useDispatch();
const getFields = useFields('filters', 'containsCaseInsensitive');
const valueInputRef = useRef<HTMLInputElement>(null);

return (
<>
<InlineSegmentGroup>
<SegmentAsync
allowCustomValue={true}
className={segmentStyles}
loadOptions={getFields}
reloadOptionsOnChange={true}
onChange={(e) => {
dispatch(changeFilterField({ id: value.id, field: e.value ?? '' }));
if (['exists', 'not exists'].includes(value.filter.operator) || isSet(value.filter.value)) {
onSubmit();
}
// Auto focus the value input when a field is selected
setTimeout(() => valueInputRef.current?.focus(), 100);
}}
placeholder="Select Field"
value={value.filter.key}
/>
<div style={{ whiteSpace: 'nowrap' }}>
<Segment
value={filterOperations.find((op) => op.value === value.filter.operator)}
options={filterOperations}
onChange={(e) => {
let op = e.value ?? filterOperations[0].value;
dispatch(changeFilterOperation({ id: value.id, op: op }));
if (['exists', 'not exists'].includes(op) || isSet(value.filter.value)) {
onSubmit();
}
}}
/>
</div>
{['=', '!=', 'term', 'not term'].includes(value.filter.operator) && (
<Input
ref={valueInputRef}
placeholder="Value"
value={value.filter.value}
onChange={(e) => dispatch(changeFilterValue({ id: value.id, value: e.currentTarget.value }))}
onKeyUp={(e) => {
if (e.key === 'Enter') {
onSubmit();
}
}}
/>
)}
{['in', 'not in'].includes(value.filter.operator) && (
<Input
ref={valueInputRef}
placeholder="Space-delimited values"
value={value.filter.value}
onChange={(e) => dispatch(changeFilterValue({ id: value.id, value: e.currentTarget.value }))}
onKeyUp={(e) => {
if (e.key === 'Enter') {
onSubmit();
}
}}
/>
)}
</InlineSegmentGroup>
</>
);
};
10 changes: 10 additions & 0 deletions src/components/QueryEditor/FilterEditor/state/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createAction } from '@reduxjs/toolkit';

import { QueryFilter } from '@/types';

export const addFilter = createAction<QueryFilter['id']>('@filters/add');
export const removeFilter = createAction<QueryFilter['id']>('@filters/remove');
export const toggleFilterVisibility = createAction<QueryFilter['id']>('@filters/toggle_visibility');
export const changeFilterField = createAction<{ id: QueryFilter['id']; field: string }>('@filters/change_field');
export const changeFilterValue = createAction<{ id: QueryFilter['id']; value: string }>('@filters/change_value');
export const changeFilterOperation = createAction<{ id: QueryFilter['id']; op: string }>('@filters/change_operation');
Loading
Loading