Skip to content

feat(work-items): add query editor for SystemLink Work Items data source - #735

Draft
shivanshu-ni wants to merge 28 commits into
mainfrom
users/shivanshu/feat/workitem-query-editor
Draft

feat(work-items): add query editor for SystemLink Work Items data source#735
shivanshu-ni wants to merge 28 commits into
mainfrom
users/shivanshu/feat/workitem-query-editor

Conversation

@shivanshu-ni

@shivanshu-ni shivanshu-ni commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Pull Request

🤨 Rationale

This PR improves Work Items query editing by adding safer default values and stronger validation.
It prevents invalid query states and makes the editor behavior more predictable.

👩‍💻 Implementation

  • Enhanced WorkItemsDataSource and WorkItemsQueryEditor logic for default query initialization.
  • Added validation handling for query input values.
  • Updated query state update flow so invalid values are not propagated.
  • Updated related work-items types and unit tests.
image

🧪 Testing

  • Updated unit tests for WorkItemsDataSource behavior.
  • Updated unit tests for WorkItemsQueryEditor default value and validation behavior.
  • Verified query change payload expectations in test coverage.

✅ Checklist

  • This PR has a title that follows the commit message format.
  • Added/updated tests.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the Work Items Grafana datasource/query editor by introducing explicit default query values and UI-side validation so queries start in a predictable state and invalid inputs are constrained before being used.

Changes:

  • Added strongly-typed query fields (output type, work item types, order-by, sort direction, take limit) plus shared constants/messages.
  • Implemented datasource-side default query initialization + normalization (prepareQuery, normalizeTypes, normalizeTake).
  • Replaced the query editor placeholder with real controls (output/type/order/descending/take) and added/updated unit tests.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/datasources/work-items/WorkItemsDataSource.ts Adds concrete defaults and query normalization via prepareQuery.
src/datasources/work-items/WorkItemsDataSource.test.ts Adds unit tests to assert defaulting and normalization behavior.
src/datasources/work-items/types.ts Introduces enums/options/constants for the new query editor + datasource behavior.
src/datasources/work-items/components/WorkItemsQueryEditor.tsx Implements the full query editor UI and validation logic.
src/datasources/work-items/components/WorkItemsQueryEditor.test.tsx Updates tests to reflect the new UI and initialization behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/datasources/work-items/components/WorkItemsQueryEditor.tsx
Comment thread src/datasources/work-items/WorkItemsDataSource.ts Outdated
Comment thread src/datasources/work-items/components/WorkItemsQueryEditor.test.tsx

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/datasources/work-items/components/WorkItemsQueryEditor.tsx:61

  • When take is invalid, the editor still calls onChange with the invalid numeric value (e.g., NaN or negative) and only suppresses onRunQuery for that one interaction. Because the invalid value is now in the persisted query state, a subsequent change (like toggling Output Type) will trigger onRunQuery with that invalid take still present.

To align with the PR goal of preventing invalid query states, avoid writing invalid take values into the query model (keep a separate draft input state and only commit to query.take when valid).

  const onTakeChange = (event: React.FormEvent<HTMLInputElement>) => {
    const value = parseInt((event.target as HTMLInputElement).value, 10);
    if (Number.isNaN(value) || value < 0) {
      setTakeInvalidMessage(takeErrorMessages.greaterOrEqualToZero);
      handleQueryChange({ ...query, take: value }, false);

src/datasources/work-items/WorkItemsDataSource.ts:48

  • prepareQuery uses nullish coalescing for take, which means invalid numeric values like NaN or negative numbers (both possible from UI input parsing) will bypass the default and be preserved in the prepared query. This can lead to invalid backend requests once runQuery starts using take.

Consider normalizing take to the default when it’s not a finite non-negative number.

      take: prepared.take ?? this.defaultQuery.take,

@shivanshu-ni
shivanshu-ni requested a review from Ahalya-ni August 19, 2026 10:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (5)

src/datasources/work-items/constants/QueryEditor.constants.ts:13

  • The take tooltip text is hard-coded to 10,000, which can drift if TAKE_LIMIT changes. Use TAKE_LIMIT in the tooltip string to keep it consistent.
  take: 'Set the maximum number of work items to return. Maximum is 10,000.',

src/datasources/work-items/WorkItemsDataSource.ts:62

  • normalizeTake currently accepts any finite non-negative number, which allows values above the editor’s documented limit (10,000). This can reintroduce invalid query states (e.g., via JSON edits) and potentially cause backend errors. Consider enforcing the same upper bound at the datasource layer.
  normalizeTake(take?: number): number {
    return Number.isFinite(take) && (take as number) >= 0 ? (take as number) : this.defaultQuery.take;
  }

src/datasources/work-items/types.ts:5

  • Empty options interfaces in this repo are typically declared on a single line (e.g., DataFrameDataSourceOptions). Keeping this one-line avoids unnecessary diffs and stays consistent with existing style.
export interface WorkItemsDataSourceOptions extends DataSourceJsonData {
}

src/datasources/work-items/constants/QueryEditor.constants.ts:6

  • takeErrorMessages.lessOrEqualToTenThousand is hard-coded to 10,000, which can drift if TAKE_LIMIT changes. Derive the message from TAKE_LIMIT to keep validation text consistent with the actual limit.

This issue also appears on line 13 of the same file.

export const takeErrorMessages = {
  greaterOrEqualToZero: 'Enter a value greater than or equal to 0',
  lessOrEqualToTenThousand: 'Enter a value less than or equal to 10,000',
};

src/datasources/work-items/WorkItemsDataSource.test.ts:28

  • This test asserts that take values above the UI/documented max (10,000) are preserved. If the datasource enforces the same limit as the query editor, the test should use an in-range value (or assert normalization behavior for out-of-range inputs).
      take: 12000,
    });

    expect(query.types).toEqual([WorkItemTypeOptions.All]);
    expect(query.take).toBe(12000);

Base automatically changed from users/shivanshu/feat/workitem-initial to main August 26, 2026 06:40
@Ahalya-ni

Copy link
Copy Markdown
Collaborator
image Follow the spacing as provided in the requirement document

@Ahalya-ni
Ahalya-ni self-requested a review August 28, 2026 16:12
Comment thread src/datasources/work-items/components/WorkItemsQueryEditor.tsx Outdated
Comment thread src/datasources/work-items/components/WorkItemsQueryEditor.tsx Outdated
Comment thread src/datasources/work-items/WorkItemsDataSource.ts
Comment thread src/datasources/work-items/components/WorkItemsQueryEditor.tsx Outdated
Comment thread src/datasources/work-items/components/WorkItemsQueryEditor.tsx Outdated
Comment thread src/datasources/work-items/components/WorkItemsQueryEditor.tsx Outdated
Comment thread src/datasources/work-items/components/WorkItemsQueryEditor.tsx Outdated
Comment thread src/datasources/work-items/WorkItemsDataSource.ts Outdated
Comment thread src/datasources/work-items/WorkItemsDataSource.ts Outdated
Comment thread src/datasources/work-items/components/WorkItemsQueryEditor.tsx Outdated
Comment thread src/datasources/work-items/components/WorkItemsQueryEditor.tsx Outdated
Comment thread src/datasources/work-items/components/WorkItemsQueryEditor.tsx Outdated
Comment thread src/datasources/work-items/README.md
Comment thread src/datasources/work-items/types.ts Outdated
Comment thread src/datasources/work-items/components/WorkItemsQueryEditor.tsx Outdated
@shivanshu-ni

Copy link
Copy Markdown
Collaborator Author

image Follow the spacing as provided in the requirement document

The Query By field and that two-column layout come with the query builder story — it isn't part of this PR. Right now all controls stack vertically since there's no builder to sit beside them. I'll align the layout with this design in the query builder PR.

Comment thread src/datasources/work-items/components/WorkItemsQueryEditor.tsx Outdated
Comment thread src/datasources/work-items/components/WorkItemsQueryEditor.tsx
Comment thread src/datasources/work-items/components/WorkItemsQueryEditor.tsx Outdated
Comment thread src/datasources/work-items/components/WorkItemsQueryEditor.tsx Outdated
Comment thread src/datasources/work-items/WorkItemsDataSource.ts
Comment thread src/datasources/work-items/WorkItemsDataSource.test.ts Outdated
Comment thread src/datasources/work-items/WorkItemsDataSource.test.ts Outdated
Comment thread src/datasources/work-items/WorkItemsDataSource.test.ts Outdated
Comment thread src/datasources/work-items/constants/QueryEditor.constants.ts Outdated
Comment thread src/datasources/work-items/components/WorkItemsQueryEditor.test.tsx Outdated
@shivanshu-ni
shivanshu-ni requested a review from richie-ni August 31, 2026 19:48
Comment thread src/datasources/work-items/components/WorkItemsQueryEditor.tsx Outdated
@shivanshu-ni
shivanshu-ni force-pushed the users/shivanshu/feat/workitem-query-editor branch from 346fc24 to f8c782e Compare September 1, 2026 06:43
@shivanshu-ni shivanshu-ni changed the title feat(work-items): add default query values and validation in query editor feat(work-items): add query editor for SystemLink Work Items data source Sep 1, 2026
@shivanshu-ni shivanshu-ni changed the title feat(work-items): add query editor for SystemLink Work Items data source feat(work-items): add query editor for SystemLink Work Items data source Sep 1, 2026
Comment thread src/datasources/work-items/constants/QueryEditor.constants.ts Outdated
Comment thread src/datasources/work-items/components/query-builder/WorkItemsQueryBuilder.tsx Outdated
Comment thread src/datasources/work-items/constants/QueryEditor.constants.ts
Comment thread src/datasources/work-items/WorkItemsDataSource.ts Outdated
…ant and clean up unused code in query editor
Comment on lines +109 to +219
<Stack
direction="column"
>
<Stack direction="column" gap={0}>
<InlineField
label={labels.outputType}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.outputType}
>
<RadioButtonGroup
options={outputTypeOptions}
onChange={onOutputTypeChange}
value={query.outputType}
/>
</InlineField>
<InlineField
label={labels.types}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.types}
invalid={!isTypesValid}
error={typesErrorMessages.atLeastOneRequired}
>
<MultiCombobox
placeholder={placeholders.types}
options={WorkItemTypes}
value={query.types}
onChange={onTypesChange}
enableAllOption
width="auto"
minWidth={CONTROL_WIDTH}
maxWidth={CONTROL_WIDTH}
/>
</InlineField>
</Stack>
{query.outputType === OutputType.Properties && (
<InlineField
label={labels.properties}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.properties}
invalid={!isPropertiesValid}
error={propertiesErrorMessages.atLeastOneRequired}
>
<MultiCombobox
placeholder={placeholders.properties}
options={propertiesOptions}
value={query.properties}
onChange={onPropertiesChange}
width="auto"
minWidth={CONTROL_WIDTH}
maxWidth={CONTROL_WIDTH}
/>
</InlineField>
)}
<Stack>
<InlineField
label={labels.queryBy}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.filter}
>
<WorkItemsQueryBuilder />
</InlineField>
{query.outputType === OutputType.Properties && (
<Stack direction="column" gap={1}>
<Stack direction="column" gap={0}>
<InlineField
label={labels.orderBy}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.orderBy}
>
<Combobox
options={OrderBy}
placeholder={placeholders.orderBy}
onChange={onOrderByChange}
value={query.orderBy}
width={COMBOBOX_WIDTH}
/>
</InlineField>
<InlineField
label={labels.descending}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.descending}
>
<InlineSwitch
onChange={event => onDescendingChange(event.currentTarget.checked)}
value={query.descending}
/>
</InlineField>
</Stack>
<InlineField
label={labels.take}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.take}
invalid={!!takeInvalidMessage}
error={takeInvalidMessage}
>
<AutoSizeInput
minWidth={COMBOBOX_WIDTH}
maxWidth={COMBOBOX_WIDTH}
type="number"
value={query.take}
onBlur={onTakeChange}
placeholder={placeholders.take}
onKeyDown={event => {
validateNumericInput(event);
}}
/>
</InlineField>
</Stack>
)}
</Stack>
</Stack>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<Stack
direction="column"
>
<Stack direction="column" gap={0}>
<InlineField
label={labels.outputType}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.outputType}
>
<RadioButtonGroup
options={outputTypeOptions}
onChange={onOutputTypeChange}
value={query.outputType}
/>
</InlineField>
<InlineField
label={labels.types}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.types}
invalid={!isTypesValid}
error={typesErrorMessages.atLeastOneRequired}
>
<MultiCombobox
placeholder={placeholders.types}
options={WorkItemTypes}
value={query.types}
onChange={onTypesChange}
enableAllOption
width="auto"
minWidth={CONTROL_WIDTH}
maxWidth={CONTROL_WIDTH}
/>
</InlineField>
</Stack>
{query.outputType === OutputType.Properties && (
<InlineField
label={labels.properties}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.properties}
invalid={!isPropertiesValid}
error={propertiesErrorMessages.atLeastOneRequired}
>
<MultiCombobox
placeholder={placeholders.properties}
options={propertiesOptions}
value={query.properties}
onChange={onPropertiesChange}
width="auto"
minWidth={CONTROL_WIDTH}
maxWidth={CONTROL_WIDTH}
/>
</InlineField>
)}
<Stack>
<InlineField
label={labels.queryBy}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.filter}
>
<WorkItemsQueryBuilder />
</InlineField>
{query.outputType === OutputType.Properties && (
<Stack direction="column" gap={1}>
<Stack direction="column" gap={0}>
<InlineField
label={labels.orderBy}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.orderBy}
>
<Combobox
options={OrderBy}
placeholder={placeholders.orderBy}
onChange={onOrderByChange}
value={query.orderBy}
width={COMBOBOX_WIDTH}
/>
</InlineField>
<InlineField
label={labels.descending}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.descending}
>
<InlineSwitch
onChange={event => onDescendingChange(event.currentTarget.checked)}
value={query.descending}
/>
</InlineField>
</Stack>
<InlineField
label={labels.take}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.take}
invalid={!!takeInvalidMessage}
error={takeInvalidMessage}
>
<AutoSizeInput
minWidth={COMBOBOX_WIDTH}
maxWidth={COMBOBOX_WIDTH}
type="number"
value={query.take}
onBlur={onTakeChange}
placeholder={placeholders.take}
onKeyDown={event => {
validateNumericInput(event);
}}
/>
</InlineField>
</Stack>
)}
</Stack>
</Stack>
<Stack direction="column" gap={0}>
<InlineField
label={labels.outputType}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.outputType}
>
<RadioButtonGroup
options={outputTypeOptions}
onChange={onOutputTypeChange}
value={query.outputType}
/>
</InlineField>
<InlineField
label={labels.types}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.types}
invalid={!isTypesValid}
error={typesErrorMessages.atLeastOneRequired}
>
<MultiCombobox
placeholder={placeholders.types}
options={WorkItemTypes}
value={query.types}
onChange={onTypesChange}
enableAllOption
width="auto"
minWidth={CONTROL_WIDTH}
maxWidth={CONTROL_WIDTH}
/>
</InlineField>
{query.outputType === OutputType.Properties && (
<>
<Space v={1} />
<InlineField
label={labels.properties}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.properties}
invalid={!isPropertiesValid}
error={propertiesErrorMessages.atLeastOneRequired}
>
<MultiCombobox
placeholder={placeholders.properties}
options={propertiesOptions}
value={query.properties}
onChange={onPropertiesChange}
width="auto"
minWidth={CONTROL_WIDTH}
maxWidth={CONTROL_WIDTH}
/>
</InlineField>
</>
)}
<Space v={1} />
<Stack>
<InlineField
label={labels.queryBy}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.filter}
>
<WorkItemsQueryBuilder />
</InlineField>
{query.outputType === OutputType.Properties && (
<Stack direction="column" gap={0}>
<InlineField
label={labels.orderBy}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.orderBy}
>
<Combobox
options={OrderBy}
placeholder={placeholders.orderBy}
onChange={onOrderByChange}
value={query.orderBy}
width={COMBOBOX_WIDTH}
/>
</InlineField>
<InlineField
label={labels.descending}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.descending}
>
<InlineSwitch
onChange={event => onDescendingChange(event.currentTarget.checked)}
value={query.descending}
/>
</InlineField>
<Space v={1} />
<InlineField
label={labels.take}
labelWidth={LABEL_WIDTH}
tooltip={tooltips.take}
invalid={!!takeInvalidMessage}
error={takeInvalidMessage}
>
<AutoSizeInput
minWidth={COMBOBOX_WIDTH}
maxWidth={COMBOBOX_WIDTH}
type="number"
value={query.take}
onBlur={onTakeChange}
placeholder={placeholders.take}
onKeyDown={event => {
validateNumericInput(event);
}}
/>
</InlineField>
</Stack>
)}
</Stack>
</Stack>

Lets use the space component to introduce the space between controls - this should reduce the number of stacks being used

Comment on lines +9 to +12
export const takeErrorMessages = {
greaterOrEqualToZero: 'Enter a value greater than or equal to 0',
lessOrEqualToTenThousand: `Enter a value less than or equal to ${TAKE_LIMIT.toLocaleString()}`,
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create a task to get this User visible strings with the PO - same goes with all the info text on each control


const onTakeChange = (event: React.FormEvent<HTMLInputElement>) => {
const value = parseInt((event.target as HTMLInputElement).value, 10);
if (Number.isNaN(value) || value <= 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing test case

return;
}

if (value > TAKE_LIMIT) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing Test case

Comment on lines +95 to +96
setTakeInvalidMessage(takeErrorMessages.greaterOrEqualToZero);
return;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have test case to check the invalid message?

render({});

expect(screen.getByText('Work item ID')).toBeTruthy();
expect(screen.getAllByText(labels.properties)[1]).toBeTruthy();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we check if all the properties are present and only Work item ID?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All 5 default properties are already verified at the data layer in WorkItemsDataSource.test.ts ('applies expected default query values'), which asserts query.properties equals the full default array (ID, NAME, TYPE, STATE, WORKSPACE) directly from prepareQuery.

This component test only smoke-checks that the editor renders the default selection, since asserting all 5 as individual DOM tags isn't reliable here — MultiCombobox collapses extra selections into a "+N" overflow badge under jsdom (no real layout is computed), so only the first tag is actually present in the DOM.

We follow the same pattern in the alarms editor (ListAlarmsQueryEditor.test.tsx) — it also never asserts multiple simultaneously-selected MultiCombobox tags, checking a single rendered property label at the component level and validating multi-value state via the handleQueryChange callback/data layer instead.


render({});

expect(screen.getByText('Work item ID')).toBeTruthy();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can move all these kinds of selectors (screen.getByText) into a page object to keep the test file clean.

Apply wherever applicable

};

// TODO: AB#3923375 - Dummy Query By scaffolding for the query editor PR
globalVariableOptions = (): QueryBuilderOption[] => this.getVariableOptions();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we introducing this in this PR ? where we have not used it anywhere

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants