The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/completions.test.ts

298 lines
8.1 KiB

Loki: query editor using Monaco (#55391) * loki: switch to a monaco-based query field, step 1 (#46291) * loki: use monaco-logql (#46318) * loki: use monaco-logql * updated monaco-logql * fix all the tests (#46327) * loki: recommend parser (#46362) * loki: recommend parser * additional improvements * more improvements * type and lint fixes * more improvements * trigger autocomplete on focus * rename * loki: more smart features (#46414) * loki: more smart features * loki: updated syntax-highlight version * better explanation (#46443) * better explanation * improved help-text Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> * Fix label * feat(loki-monaco-editor): add monaco-logql as a dependency * feat(loki-monaco-editor): add back range function removed during merge * feat(loki-monaco-editor): sync imports with recent changes * feat(loki-monaco-editor): add missing lang provider functions * feat(loki-monaco-editor): fix imports * feat(loki-monaco-editor): display monaco editor by default Temporarily * Chore: remove commented code * Chore: minor refactor to NeverCaseError * Chore: minor code cleanups * feat(loki-monaco-editor): add history implementation Will see how it behaves and base the history slicing on tangible feedback * feat(loki-monaco-editor): turn completion data provider into a class * Chore: fix missing imports * feat(loki-monaco-editor): refactor data provider methods Move complexity scattered everywhere to the provider * Chore: clean up redundant code * Chore: minor comments cleanup * Chore: simplify override services * Chore: rename callback * feat(loki-monaco-editor): use query hints implementation to parse expression * feat(loki-monaco-editor): improve function name * Chore: remove superfluous variable in favor of destructuring * Chore: remove unused imports * Chore: make method async * feat(loki-monaco-editor): fix deprecations and errors in situation * feat(loki-monaco-editor): comment failing test case * Chore: remove comment from test * Chore: remove duplicated completion item * Chore: fix linting issues * Chore: update language provider test * Chore: update datasource test * feat(loki-monaco-editor): create feature flag * feat(loki-monaco-editor): place the editor under a feature flag * Chore: add completion unit test * Chore: add completion data provider test * Chore: remove unwanted export * Chore: remove unused export * Chore(loki-query-field): destructure all props * chore(loki-completions): remove odd string * fix(loki-completions): remove rate_interval Not supported * fix(loki-completions): remove line filters for after pipe case We shouldn't offer line filters if we are after first pipe. * refactor(loki-datasource): update default parameter * fix(loki-syntax): remove outdated documentation * Update capitalization in pkg/services/featuremgmt/registry.go Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> * refactor(situation): use node types instead of names * Chore: comment line filters pending implementation It's breaking the build due to a linting error. * Chore: update feature flag test after capitalization change * Revert "fix(loki-completions): remove line filters for after pipe case" This reverts commit 3d003ca4bcb792b440add04b883fafe637e4c4c9. * Revert "Chore: comment line filters pending implementation" This reverts commit 84bfe76a6a29e79a15a3e080929d1f387691700b. Co-authored-by: Gábor Farkas <gabor.farkas@gmail.com> Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Co-authored-by: Ivana Huckova <ivana.huckova@gmail.com>
3 years ago
import LokiLanguageProvider from '../../../LanguageProvider';
import { LokiDatasource } from '../../../datasource';
import { createLokiDatasource } from '../../../mocks';
import { CompletionDataProvider } from './CompletionDataProvider';
import { getCompletions } from './completions';
import { Label, Situation } from './situation';
const history = [
{
ts: 12345678,
query: {
refId: 'test-1',
expr: '{test: unit}',
},
},
{
ts: 87654321,
query: {
refId: 'test-1',
expr: '{test: unit}',
},
},
];
const labelNames = ['place', 'source'];
const labelValues = ['moon', 'luna'];
const extractedLabelKeys = ['extracted', 'label'];
const otherLabels: Label[] = [
{
name: 'place',
value: 'luna',
op: '=',
},
];
const afterSelectorCompletions = [
{
insertText: '|= "$0"',
isSnippet: true,
label: '|= ""',
type: 'LINE_FILTER',
},
{
insertText: '!= "$0"',
isSnippet: true,
label: '!= ""',
type: 'LINE_FILTER',
},
{
insertText: '|~ "$0"',
isSnippet: true,
label: '|~ ""',
type: 'LINE_FILTER',
},
{
insertText: '!~ "$0"',
isSnippet: true,
label: '!~ ""',
type: 'LINE_FILTER',
},
{
insertText: '',
label: '// Placeholder for the detected parser',
type: 'DETECTED_PARSER_PLACEHOLDER',
},
{
insertText: '',
label: '// Placeholder for logfmt or json',
type: 'OPPOSITE_PARSER_PLACEHOLDER',
},
{
insertText: 'pattern',
label: 'pattern',
type: 'PARSER',
},
{
insertText: 'regexp',
label: 'regexp',
type: 'PARSER',
},
{
insertText: 'unpack',
label: 'unpack',
type: 'PARSER',
},
{
insertText: 'unwrap extracted',
label: 'unwrap extracted (detected)',
type: 'LINE_FILTER',
},
{
insertText: 'unwrap label',
label: 'unwrap label (detected)',
type: 'LINE_FILTER',
},
{
insertText: 'unwrap',
label: 'unwrap',
type: 'LINE_FILTER',
},
{
insertText: 'line_format "{{.$0}}"',
isSnippet: true,
label: 'line_format',
type: 'LINE_FORMAT',
},
];
function buildAfterSelectorCompletions(
detectedParser: string,
detectedParserType: string,
otherParser: string,
explanation = '(detected)'
) {
return afterSelectorCompletions.map((completion) => {
if (completion.type === 'DETECTED_PARSER_PLACEHOLDER') {
return {
...completion,
type: detectedParserType,
label: `${detectedParser} ${explanation}`,
insertText: detectedParser,
};
} else if (completion.type === 'OPPOSITE_PARSER_PLACEHOLDER') {
return {
...completion,
type: 'PARSER',
label: otherParser,
insertText: otherParser,
};
}
return { ...completion };
});
}
describe('getCompletions', () => {
let completionProvider: CompletionDataProvider, languageProvider: LokiLanguageProvider, datasource: LokiDatasource;
beforeEach(() => {
datasource = createLokiDatasource();
languageProvider = new LokiLanguageProvider(datasource);
completionProvider = new CompletionDataProvider(languageProvider, history);
jest.spyOn(completionProvider, 'getLabelNames').mockResolvedValue(labelNames);
jest.spyOn(completionProvider, 'getLabelValues').mockResolvedValue(labelValues);
jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({
extractedLabelKeys,
hasJSON: false,
hasLogfmt: false,
});
});
test.each(['EMPTY', 'AT_ROOT'])(`Returns completion options when the situation is %s`, async (type) => {
const situation = { type } as Situation;
const completions = await getCompletions(situation, completionProvider);
expect(completions).toHaveLength(25);
});
test('Returns completion options when the situation is IN_DURATION', async () => {
const situation: Situation = { type: 'IN_DURATION' };
const completions = await getCompletions(situation, completionProvider);
expect(completions).toEqual([
{ insertText: '$__interval', label: '$__interval', type: 'DURATION' },
{ insertText: '$__range', label: '$__range', type: 'DURATION' },
{ insertText: '1m', label: '1m', type: 'DURATION' },
{ insertText: '5m', label: '5m', type: 'DURATION' },
{ insertText: '10m', label: '10m', type: 'DURATION' },
{ insertText: '30m', label: '30m', type: 'DURATION' },
{ insertText: '1h', label: '1h', type: 'DURATION' },
{ insertText: '1d', label: '1d', type: 'DURATION' },
]);
});
test('Returns completion options when the situation is IN_GROUPING', async () => {
const situation: Situation = { type: 'IN_GROUPING', otherLabels };
const completions = await getCompletions(situation, completionProvider);
expect(completions).toEqual([
{
insertText: 'place',
label: 'place',
triggerOnInsert: false,
type: 'LABEL_NAME',
},
{
insertText: 'source',
label: 'source',
triggerOnInsert: false,
type: 'LABEL_NAME',
},
{
insertText: 'extracted',
label: 'extracted (parsed)',
triggerOnInsert: false,
type: 'LABEL_NAME',
},
{
insertText: 'label',
label: 'label (parsed)',
triggerOnInsert: false,
type: 'LABEL_NAME',
},
]);
});
test('Returns completion options when the situation is IN_LABEL_SELECTOR_NO_LABEL_NAME', async () => {
const situation: Situation = { type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME', otherLabels };
const completions = await getCompletions(situation, completionProvider);
expect(completions).toEqual([
{
insertText: 'place=',
label: 'place',
triggerOnInsert: true,
type: 'LABEL_NAME',
},
{
insertText: 'source=',
label: 'source',
triggerOnInsert: true,
type: 'LABEL_NAME',
},
]);
});
test('Returns completion options when the situation is IN_LABEL_SELECTOR_WITH_LABEL_NAME', async () => {
const situation: Situation = {
type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
otherLabels,
labelName: '',
betweenQuotes: false,
};
let completions = await getCompletions(situation, completionProvider);
expect(completions).toEqual([
{
insertText: '"moon"',
label: 'moon',
type: 'LABEL_VALUE',
},
{
insertText: '"luna"',
label: 'luna',
type: 'LABEL_VALUE',
},
]);
completions = await getCompletions({ ...situation, betweenQuotes: true }, completionProvider);
expect(completions).toEqual([
{
insertText: 'moon',
label: 'moon',
type: 'LABEL_VALUE',
},
{
insertText: 'luna',
label: 'luna',
type: 'LABEL_VALUE',
},
]);
});
test('Returns completion options when the situation is AFTER_SELECTOR and JSON parser', async () => {
jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({
extractedLabelKeys,
hasJSON: true,
hasLogfmt: false,
});
const situation: Situation = { type: 'AFTER_SELECTOR', labels: [], afterPipe: true };
const completions = await getCompletions(situation, completionProvider);
const expected = buildAfterSelectorCompletions('json', 'PARSER', 'logfmt');
expect(completions).toEqual(expected);
});
test('Returns completion options when the situation is AFTER_SELECTOR and Logfmt parser', async () => {
jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({
extractedLabelKeys,
hasJSON: false,
hasLogfmt: true,
});
const situation: Situation = { type: 'AFTER_SELECTOR', labels: [], afterPipe: true };
const completions = await getCompletions(situation, completionProvider);
const expected = buildAfterSelectorCompletions('logfmt', 'DURATION', 'json');
expect(completions).toEqual(expected);
});
test('Returns completion options when the situation is IN_AGGREGATION', async () => {
const situation: Situation = { type: 'IN_AGGREGATION' };
const completions = await getCompletions(situation, completionProvider);
expect(completions).toHaveLength(22);
});
});