-
Notifications
You must be signed in to change notification settings - Fork 6
feat(work-items): add query editor for SystemLink Work Items data source #735
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
shivanshu-ni
wants to merge
36
commits into
main
Choose a base branch
from
users/shivanshu/feat/workitem-query-editor
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+829
−19
Open
Changes from all commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
bdcb741
Feat(work-items): Initial Boilerplate
shivanshu-ni 769e5fb
Added workitems in example.yaml
shivanshu-ni a3b89df
feat(work-items): add SystemLink Work Items datasource configuration …
shivanshu-ni e28b279
feat(work-items): enhance WorkItemsDataSource and QueryEditor with de…
shivanshu-ni f287de3
feat(work-items): update datasource connection method to use POST for…
shivanshu-ni bb25723
refactor(work-items): remove unused WorkItemsConfigEditor and clean u…
shivanshu-ni fa68b82
refactor(work-items): remove Work Items datasource configuration from…
shivanshu-ni cd898db
Merge branch 'users/shivanshu/feat/workitem-initial' of https://githu…
shivanshu-ni efbfdfc
feat(work-items): refactor query editor and data source logic, add co…
shivanshu-ni ed599a2
feat(work-items): update take value handling in query editor and data…
shivanshu-ni ea014e8
feat(work-items): implement take value normalization and validation i…
shivanshu-ni 7b0dd78
feat(work-items): enhance take value handling in query editor and dat…
shivanshu-ni 08a6d2f
feat(docs): add README for SystemLink Work Items Data Source plugin
shivanshu-ni 15cdb5f
feat(work-items): add initial README for SystemLink Work Items dataso…
shivanshu-ni 29587bd
refactor(work-items): simplify WorkItemsDataSource and update query e…
shivanshu-ni 10b0041
Merge branch 'users/shivanshu/feat/workitem-initial' of https://githu…
shivanshu-ni 51cf0bf
refactor(work-items): remove unused import from WorkItemsDataSource
shivanshu-ni 32294b0
refactor(work-items): extract query editor labels into constants file
shivanshu-ni 20bbb26
Merge branch 'main' of https://github.com/ni/systemlink-grafana-plugi…
shivanshu-ni 7c79ab4
refactor(tests): consolidate imports in WorkItemsQueryEditor test file
shivanshu-ni 91938ef
feat(work-items): add properties selection to WorkItemsQueryEditor an…
shivanshu-ni 1b41eb9
fix(work-items): disambiguate Properties label lookup in query editor…
shivanshu-ni e7494ab
Add work-items acceptance test placeholder
Copilot 7c05a1a
fix(work-items): address review feedback
shivanshu-ni f8c782e
fix(work-items): enhance WorkItemsQueryEditor with validation and que…
shivanshu-ni 713f5e7
fix(work-items): Group order by and decending in one group
shivanshu-ni 9f11b72
fix(work-items): replace hardcoded take value with DEFAULT_TAKE const…
shivanshu-ni d05b387
feat(work-items): update default query properties (#738)
shivanshu-ni d0afbcb
fix(work-items): streamline WorkItemsQueryEditor layout and remove un…
shivanshu-ni 785eea3
Merge branch 'users/shivanshu/feat/workitem-query-editor' of https://…
shivanshu-ni 11f8c61
fix(tests): adjust offsetHeight mock and enhance properties output va…
shivanshu-ni 7023866
fix(tests): update userEvent usage in WorkItemsQueryEditor tests for …
shivanshu-ni 45adbd1
fix(tests): remove unnecessary typing in WorkItemsQueryEditor test fo…
shivanshu-ni 3c0f23d
fix(tests): simplify user interactions in WorkItemsQueryEditor tests …
shivanshu-ni f6c46f4
fix(tests): update WorkItemsQueryEditor tests to use new combobox and…
shivanshu-ni 85d20e8
fix(tests): update WorkItemsQueryEditor and WorkItemsDataSource tests…
shivanshu-ni File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
src/datasources/work-items/components/WorkItemsQueryEditor.page.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { fireEvent, screen } from '@testing-library/react'; | ||
| import { OutputType } from '../types'; | ||
| import { labels } from '../constants/QueryEditor.constants'; | ||
|
|
||
| /** | ||
| * Page object collecting the DOM selectors used by WorkItemsQueryEditor.test.tsx, | ||
| * so the test file only reads intent, not query-library boilerplate. | ||
| */ | ||
| export const workItemsQueryEditorPage = { | ||
| outputTypeRadioButton: (value: OutputType) => screen.getByRole('radio', { name: value }), | ||
|
|
||
| // MultiCombobox (used for Types/Properties) doesn't forward its id to the underlying | ||
| // downshift input, so it has no accessible name; select by position among comboboxes instead. | ||
| typesMultiCombobox: () => screen.queryAllByRole('combobox')[0] ?? null, | ||
| propertiesMultiCombobox: () => screen.queryAllByRole('combobox')[1] ?? null, | ||
| orderByCombobox: () => screen.queryByRole('combobox', { name: labels.orderBy }), | ||
| descendingSwitch: () => screen.queryByRole('switch', { name: labels.descending }), | ||
| takeLimitInput: () => screen.getByRole('spinbutton'), | ||
| optionalTakeLimitInput: () => screen.queryByRole('spinbutton'), | ||
| setTakeLimit: (value: string) => { | ||
| const takeLimitInput = screen.getByRole('spinbutton'); | ||
| fireEvent.change(takeLimitInput, { target: { value } }); | ||
| fireEvent.blur(takeLimitInput); | ||
| }, | ||
|
|
||
| removeOptionButton: (name: string) => screen.getByRole('button', { name: `Remove ${name}` }), | ||
| typeSelectOption: (name: string) => screen.findByRole('option', { name }), | ||
| propertySelectOption: (name: string) => screen.findByRole('option', { name }), | ||
| propertyOptionCheckbox: (name: string) => screen.getByRole('checkbox', { name }), | ||
|
|
||
| getErrorByMessage: (message: string) => screen.queryByText(message), | ||
| }; |
159 changes: 156 additions & 3 deletions
159
src/datasources/work-items/components/WorkItemsQueryEditor.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,167 @@ | ||
| import { screen } from '@testing-library/react'; | ||
| import { fireEvent } from '@testing-library/react'; | ||
| import userEvent from '@testing-library/user-event'; | ||
| import { setupRenderer } from 'test/fixtures'; | ||
| import { propertiesErrorMessages, takeErrorMessages, typesErrorMessages } from '../constants/QueryEditor.constants'; | ||
| import { TAKE_LIMIT } from '../constants'; | ||
| import { WorkItemsDataSource } from '../WorkItemsDataSource'; | ||
| import { OutputType, WorkItemPropertiesOptions, WorkItemTypeOptions } from '../types'; | ||
| import { WorkItemsQueryEditor } from './WorkItemsQueryEditor'; | ||
| import { workItemsQueryEditorPage as page } from './WorkItemsQueryEditor.page'; | ||
|
|
||
| describe('WorkItemsQueryEditor', () => { | ||
| it('shows placeholder message', () => { | ||
| it('should show all controls when the editor renders', () => { | ||
| const render = setupRenderer(WorkItemsQueryEditor, WorkItemsDataSource); | ||
|
|
||
| render({}); | ||
|
|
||
| expect(screen.getByText('Work Items datasource query controls will be added in follow-up stories.')).toBeInTheDocument(); | ||
| expect(page.outputTypeRadioButton(OutputType.Properties)).toBeInTheDocument(); | ||
| expect(page.outputTypeRadioButton(OutputType.TotalCount)).toBeInTheDocument(); | ||
| expect(page.typesMultiCombobox()).toBeVisible(); | ||
| expect(page.propertiesMultiCombobox()).toBeVisible(); | ||
| expect(page.orderByCombobox()).toBeVisible(); | ||
| expect(page.descendingSwitch()).toBeInTheDocument(); | ||
| expect(page.optionalTakeLimitInput()).toBeVisible(); | ||
| }); | ||
|
|
||
| it('should hide properties-only controls when the output type is total count', async () => { | ||
| const render = setupRenderer(WorkItemsQueryEditor, WorkItemsDataSource); | ||
|
|
||
| render({}); | ||
| await userEvent.click(page.outputTypeRadioButton(OutputType.TotalCount)); | ||
|
|
||
| expect(page.propertiesMultiCombobox()).toBeNull(); | ||
| expect(page.orderByCombobox()).toBeNull(); | ||
| expect(page.descendingSwitch()).toBeNull(); | ||
| expect(page.optionalTakeLimitInput()).toBeNull(); | ||
| }); | ||
|
|
||
| it('should show default selected properties when the output type is properties', async () => { | ||
| const offsetHeightSpy = jest.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(30); | ||
|
|
||
| try { | ||
| const render = setupRenderer(WorkItemsQueryEditor, WorkItemsDataSource); | ||
| render({}); | ||
|
|
||
| const propertiesCombobox = page.propertiesMultiCombobox()!; | ||
| const checkedLabels = ['Work item name', 'State', 'Assigned to', 'Planned start date', 'Due date']; | ||
|
|
||
| fireEvent.click(propertiesCombobox); | ||
| for (const label of checkedLabels) { | ||
|
shivanshu-ni marked this conversation as resolved.
|
||
| // fireEvent.change is used here to filter/search the dropdown options; it does not select or deselect them. | ||
| fireEvent.change(propertiesCombobox, { target: { value: label } }); | ||
| expect(page.propertyOptionCheckbox(label)).toBeChecked(); | ||
| } | ||
|
|
||
| fireEvent.change(propertiesCombobox, { target: { value: 'Work item ID' } }); | ||
| expect(page.propertyOptionCheckbox('Work item ID')).not.toBeChecked(); | ||
| } finally { | ||
| offsetHeightSpy.mockRestore(); | ||
| } | ||
| }); | ||
|
|
||
| describe('validation error', () => { | ||
| it('should not show types, properties, or take validation errors when the editor renders', () => { | ||
| const render = setupRenderer(WorkItemsQueryEditor, WorkItemsDataSource); | ||
|
|
||
| render({}); | ||
|
|
||
| expect(page.getErrorByMessage(typesErrorMessages.atLeastOneRequired)).toBeNull(); | ||
| expect(page.getErrorByMessage(propertiesErrorMessages.atLeastOneRequired)).toBeNull(); | ||
| expect(page.getErrorByMessage(takeErrorMessages.greaterOrEqualToZero)).toBeNull(); | ||
| expect(page.getErrorByMessage(takeErrorMessages.lessOrEqualToTenThousand)).toBeNull(); | ||
| }); | ||
|
|
||
| it('should clear the types validation error when a type is re-added after all types are removed', async () => { | ||
| const offsetHeightSpy = jest.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(30); | ||
|
|
||
| try { | ||
| const render = setupRenderer(WorkItemsQueryEditor, WorkItemsDataSource); | ||
| const [onChange, onRunQuery] = render({ types: [WorkItemTypeOptions.WorkOrders] }); | ||
|
|
||
| await userEvent.click(page.removeOptionButton('Work orders')); | ||
|
|
||
| expect(page.getErrorByMessage(typesErrorMessages.atLeastOneRequired)).toBeVisible(); | ||
| expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({ types: [] })); | ||
| expect(onRunQuery).not.toHaveBeenCalled(); | ||
|
|
||
| const typesCombobox = page.typesMultiCombobox()!; | ||
| await userEvent.click(typesCombobox); | ||
| await userEvent.click(await page.typeSelectOption('Work orders')); | ||
|
|
||
| expect(page.getErrorByMessage(typesErrorMessages.atLeastOneRequired)).toBeNull(); | ||
| expect(onChange).toHaveBeenLastCalledWith( | ||
| expect.objectContaining({ types: [WorkItemTypeOptions.WorkOrders] }) | ||
| ); | ||
| expect(onRunQuery).toHaveBeenCalled(); | ||
| } finally { | ||
| offsetHeightSpy.mockRestore(); | ||
| } | ||
| }); | ||
|
|
||
| it('should clear the properties validation error when a property is re-added after all properties are removed', async () => { | ||
| const offsetHeightSpy = jest.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(30); | ||
|
|
||
| try { | ||
| const render = setupRenderer(WorkItemsQueryEditor, WorkItemsDataSource); | ||
| const [onChange, onRunQuery] = render({ properties: [WorkItemPropertiesOptions.ID] }); | ||
|
|
||
| await userEvent.click(page.removeOptionButton('Work item ID')); | ||
|
|
||
| expect(page.getErrorByMessage(propertiesErrorMessages.atLeastOneRequired)).toBeVisible(); | ||
| expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({ properties: [] })); | ||
| expect(onRunQuery).not.toHaveBeenCalled(); | ||
|
|
||
| const propertiesCombobox = page.propertiesMultiCombobox()!; | ||
| await userEvent.click(propertiesCombobox); | ||
| await userEvent.click(await page.propertySelectOption('Work item name')); | ||
|
|
||
| expect(page.getErrorByMessage(propertiesErrorMessages.atLeastOneRequired)).toBeNull(); | ||
| expect(onChange).toHaveBeenLastCalledWith( | ||
| expect.objectContaining({ properties: [WorkItemPropertiesOptions.NAME] }) | ||
| ); | ||
| expect(onRunQuery).toHaveBeenCalled(); | ||
| } finally { | ||
| offsetHeightSpy.mockRestore(); | ||
| } | ||
| }); | ||
|
|
||
| it('should show a take validation error and suppress query execution when take input is invalid', () => { | ||
| const render = setupRenderer(WorkItemsQueryEditor, WorkItemsDataSource); | ||
|
|
||
| const [onChange, onRunQuery] = render({}); | ||
|
|
||
| page.setTakeLimit('-5'); | ||
|
|
||
| expect(page.getErrorByMessage(takeErrorMessages.greaterOrEqualToZero)).toBeVisible(); | ||
| expect(onChange).not.toHaveBeenCalled(); | ||
| expect(onRunQuery).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('should show a take validation error and suppress query execution when take exceeds the maximum limit', () => { | ||
| const render = setupRenderer(WorkItemsQueryEditor, WorkItemsDataSource); | ||
|
|
||
| const [onChange, onRunQuery] = render({}); | ||
|
|
||
| page.setTakeLimit(`${TAKE_LIMIT + 1}`); | ||
|
|
||
| expect(page.getErrorByMessage(takeErrorMessages.lessOrEqualToTenThousand)).toBeVisible(); | ||
| expect(onChange).not.toHaveBeenCalled(); | ||
| expect(onRunQuery).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('should clear the take validation error and run the query when a valid take value is entered', () => { | ||
| const render = setupRenderer(WorkItemsQueryEditor, WorkItemsDataSource); | ||
|
|
||
| const [onChange, onRunQuery] = render({}); | ||
|
|
||
| page.setTakeLimit('-5'); | ||
| expect(page.getErrorByMessage(takeErrorMessages.greaterOrEqualToZero)).toBeVisible(); | ||
|
|
||
| page.setTakeLimit('500'); | ||
|
|
||
| expect(page.getErrorByMessage(takeErrorMessages.greaterOrEqualToZero)).toBeNull(); | ||
| expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({ take: 500 })); | ||
| expect(onRunQuery).toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.