-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support creating issues for specific measurement entities. Closes #5955.
- Loading branch information
Showing
16 changed files
with
313 additions
and
224 deletions.
There are no files selected for viewing
This file contains 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 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 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
File renamed without changes.
File renamed without changes.
This file contains 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,111 @@ | ||
import React, { useState } from 'react'; | ||
import { Grid } from 'semantic-ui-react'; | ||
import { set_metric_attribute, add_metric_issue } from '../api/metric'; | ||
import { get_report_issue_tracker_suggestions } from '../api/report'; | ||
import { EDIT_REPORT_PERMISSION, ReadOnlyOrEditable } from '../context/Permissions'; | ||
import { MultipleChoiceInput } from '../fields/MultipleChoiceInput'; | ||
import { ActionButton } from '../widgets/Button'; | ||
import { LabelWithHelp } from '../widgets/LabelWithHelp'; | ||
import { ErrorMessage } from '../errorMessage'; | ||
import { get_metric_issue_ids } from '../utils'; | ||
|
||
function CreateIssueButton({ entityKey, issueTrackerConfigured, issueTrackerInstruction, metric_uuid, target, reload }) { | ||
return ( | ||
<ActionButton | ||
action='Create new' | ||
disabled={!issueTrackerConfigured} | ||
fluid | ||
icon='plus' | ||
item_type='issue' | ||
onClick={() => add_metric_issue(entityKey, metric_uuid, reload)} | ||
popup={<>Create a new issue for this {target} in the configured issue tracker and add its identifier to the tracked issue identifiers.{issueTrackerInstruction}</>} | ||
position='top center' | ||
/> | ||
) | ||
} | ||
|
||
function IssueIdentifiers({ issue_tracker_instruction, metric, metric_uuid, report_uuid, target, reload }) { | ||
const issueStatusHelp = ( | ||
<> | ||
<p>Identifiers of issues in the configured issue tracker that track the progress of fixing this {target}.</p> | ||
<p>When the issues have all been resolved, or the technical debt end date has passed, whichever happens first, the technical debt should be resolved and the technical debt target is no longer evaluated.</p> | ||
{issue_tracker_instruction} | ||
</> | ||
) | ||
const [suggestions, setSuggestions] = useState([]); | ||
const labelId = `issue-identifiers-label-${metric_uuid}` | ||
const issue_ids = get_metric_issue_ids(metric); | ||
return ( | ||
<MultipleChoiceInput | ||
aria-labelledby={labelId} | ||
allowAdditions | ||
onSearchChange={(query) => { | ||
if (query) { | ||
get_report_issue_tracker_suggestions(report_uuid, query).then((suggestionsResponse) => { | ||
const suggestionOptions = suggestionsResponse.suggestions.map((s) => ({ key: s.key, text: `${s.key}: ${s.text}`, value: s.key })) | ||
setSuggestions(suggestionOptions) | ||
}) | ||
} else { | ||
setSuggestions([]) | ||
} | ||
}} | ||
requiredPermissions={[EDIT_REPORT_PERMISSION]} | ||
label={<LabelWithHelp labelId={labelId} label="Issue identifiers" help={issueStatusHelp} />} | ||
options={suggestions} | ||
set_value={(value) => set_metric_attribute(metric_uuid, "issue_ids", value, reload)} | ||
value={issue_ids} | ||
key={issue_ids} // Make sure the multiple choice input is rerendered when the issue ids change | ||
/> | ||
) | ||
} | ||
|
||
export function IssuesRows({ entityKey, metric, metric_uuid, reload, report, target }) { | ||
const parameters = report?.issue_tracker?.parameters; | ||
const issueTrackerConfigured = report?.issue_tracker?.type && parameters?.url && parameters?.project_key && parameters?.issue_type; | ||
const issueTrackerInstruction = issueTrackerConfigured ? null : <p>Please configure an issue tracker by expanding the report title, selecting the 'Issue tracker' tab, and configuring an issue tracker.</p>; | ||
return ( | ||
<> | ||
<Grid.Row> | ||
<ReadOnlyOrEditable | ||
requiredPermissions={[EDIT_REPORT_PERMISSION]} | ||
readOnlyComponent={ | ||
<Grid.Column width={16}> | ||
<IssueIdentifiers issue_tracker_instruction={issueTrackerInstruction} metric={metric} metric_uuid={metric_uuid} report_uuid={report.report_uuid} target={target ?? "metric"} reload={reload} /> | ||
</Grid.Column> | ||
} | ||
editableComponent={ | ||
<> | ||
< Grid.Column width={3} verticalAlign="bottom"> | ||
<CreateIssueButton entityKey={entityKey} issueTrackerConfigured={issueTrackerConfigured} issueTrackerInstruction={issueTrackerInstruction} metric_uuid={metric_uuid} target={target ?? "metric"} reload={reload} /> | ||
</Grid.Column> | ||
<Grid.Column width={13}> | ||
<IssueIdentifiers issue_tracker_instruction={issueTrackerInstruction} metric={metric} metric_uuid={metric_uuid} report_uuid={report.report_uuid} target={target ?? "metric"} reload={reload} /> | ||
</Grid.Column> | ||
</> | ||
} | ||
/> | ||
</Grid.Row> | ||
{(get_metric_issue_ids(metric).length > 0 && !issueTrackerConfigured) && | ||
<Grid.Row> | ||
<Grid.Column width={16}> | ||
<ErrorMessage title="No issue tracker configured" message={issueTrackerInstruction} /> | ||
</Grid.Column> | ||
</Grid.Row> | ||
} | ||
{(metric.issue_status ?? []).filter((issue_status => issue_status.connection_error)).map((issue_status) => | ||
<Grid.Row key={issue_status.issue_id}> | ||
<Grid.Column width={16}> | ||
<ErrorMessage key={issue_status.issue_id} title={"Connection error while retrieving " + issue_status.issue_id} message={issue_status.connection_error} /> | ||
</Grid.Column> | ||
</Grid.Row> | ||
)} | ||
{(metric.issue_status ?? []).filter((issue_status => issue_status.parse_error)).map((issue_status) => | ||
<Grid.Row key={issue_status.issue_id}> | ||
<Grid.Column width={16}> | ||
<ErrorMessage key={issue_status.issue_id} title={"Parse error while processing " + issue_status.issue_id} message={issue_status.parse_error} /> | ||
</Grid.Column> | ||
</Grid.Row> | ||
)} | ||
</> | ||
) | ||
} |
This file contains 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,112 @@ | ||
import React from 'react'; | ||
import { fireEvent, render, screen } from '@testing-library/react'; | ||
import userEvent from '@testing-library/user-event'; | ||
import { EDIT_REPORT_PERMISSION, Permissions } from '../context/Permissions'; | ||
import { IssuesRows } from './IssuesRows'; | ||
import * as fetch_server_api from '../api/fetch_server_api'; | ||
|
||
jest.mock("../api/fetch_server_api.js") | ||
|
||
const reportWithIssueTracker = { | ||
issue_tracker: { type: "Jira", parameters: { url: "https://jira", project_key: "KEY", issue_type: "Bug" } } | ||
} | ||
|
||
function renderIssuesRow( | ||
{ | ||
issue_ids = [], | ||
report = { subjects: {} }, | ||
permissions = [EDIT_REPORT_PERMISSION], | ||
issue_status = [] | ||
} = {}) { | ||
render( | ||
<Permissions.Provider value={permissions}> | ||
<IssuesRows | ||
metric={ | ||
{ | ||
type: "violations", | ||
issue_ids: issue_ids, | ||
issue_status: issue_status | ||
} | ||
} | ||
metric_uuid="metric_uuid" | ||
reload={jest.fn} | ||
report={report} | ||
/> | ||
</Permissions.Provider> | ||
); | ||
} | ||
|
||
it('does not show an error message if the metric has no issues and no issue tracker is configured', () => { | ||
renderIssuesRow() | ||
expect(screen.queryAllByText(/No issue tracker configured/).length).toBe(0); | ||
}); | ||
|
||
it('does not show an error message if the metric has no issues and an issue tracker is configured', () => { | ||
renderIssuesRow({ report: { issue_tracker: { type: "Jira" } } }) | ||
expect(screen.queryAllByText(/No issue tracker configured/).length).toBe(0); | ||
}); | ||
|
||
it('does not show an error message if the metric has issues and an issue tracker is configured', () => { | ||
renderIssuesRow({ issue_ids: ["BAR-42"], report: { issue_tracker: { type: "Jira", parameters: { url: "https://jira", project_key: "KEY", issue_type: "Bug" } } } }) | ||
expect(screen.queryAllByText(/No issue tracker configured/).length).toBe(0); | ||
}); | ||
|
||
it('shows an error message if the metric has issues but no issue tracker is configured', () => { | ||
renderIssuesRow({ issue_ids: ["FOO-42"] }) | ||
expect(screen.queryAllByText(/No issue tracker configured/).length).toBe(1); | ||
}); | ||
|
||
it('shows a connection error', () => { | ||
renderIssuesRow({ issue_status: [{ issue_id: "FOO-43", connection_error: "Oops" }] }) | ||
expect(screen.queryAllByText(/Connection error/).length).toBe(1); | ||
expect(screen.queryAllByText(/Oops/).length).toBe(1); | ||
}); | ||
|
||
it('shows a parse error', () => { | ||
renderIssuesRow({ issue_status: [{ issue_id: "FOO-43", parse_error: "Oops" }] }) | ||
expect(screen.queryAllByText(/Parse error/).length).toBe(1); | ||
expect(screen.queryAllByText(/Oops/).length).toBe(1); | ||
}); | ||
|
||
it('creates an issue', () => { | ||
window.open = jest.fn() | ||
fetch_server_api.fetch_server_api = jest.fn().mockResolvedValue({ ok: true, error: "", issue_url: "https://tracker/foo-42" }); | ||
renderIssuesRow({ report: reportWithIssueTracker }) | ||
fireEvent.click(screen.getByText(/Create new issue/)) | ||
expect(fetch_server_api.fetch_server_api).toHaveBeenLastCalledWith("post", "metric/metric_uuid/issue/new", { metric_url: "http://localhost/#metric_uuid" }); | ||
}); | ||
|
||
it('tries to create an issue', () => { | ||
fetch_server_api.fetch_server_api = jest.fn().mockResolvedValue({ ok: false, error: "Dummy", issue_url: "" }); | ||
renderIssuesRow({ report: reportWithIssueTracker }) | ||
fireEvent.click(screen.getByText(/Create new issue/)) | ||
expect(fetch_server_api.fetch_server_api).toHaveBeenLastCalledWith("post", "metric/metric_uuid/issue/new", { metric_url: "http://localhost/#metric_uuid" }); | ||
}); | ||
|
||
it('does not show the create issue button if the user has no permissions', () => { | ||
renderIssuesRow({ report: reportWithIssueTracker, permissions: [] }) | ||
expect(screen.queryAllByText(/Create new issue/).length).toBe(0) | ||
}) | ||
|
||
it('adds an issue id', async () => { | ||
fetch_server_api.fetch_server_api = jest.fn().mockResolvedValue({ suggestions: [{ key: "FOO-42", text: "Suggestion" }] }); | ||
renderIssuesRow() | ||
await userEvent.type(screen.getByLabelText(/Issue identifiers/), 'FOO-42{Enter}'); | ||
expect(fetch_server_api.fetch_server_api).toHaveBeenLastCalledWith("post", "metric/metric_uuid/attribute/issue_ids", { issue_ids: ["FOO-42"] }); | ||
}); | ||
|
||
it('shows issue id suggestions', async () => { | ||
fetch_server_api.fetch_server_api = jest.fn().mockResolvedValue({ suggestions: [{ key: "FOO-42", text: "Suggestion" }] }); | ||
renderIssuesRow({ report: { issue_tracker: { type: "Jira", parameters: { url: "https://jira" } } } }) | ||
await userEvent.type(screen.getByLabelText(/Issue identifiers/), 'u'); | ||
expect(screen.queryAllByText(/FOO-42: Suggestion/).length).toBe(1) | ||
}); | ||
|
||
it('shows no issue id suggestions without a query', async () => { | ||
fetch_server_api.fetch_server_api = jest.fn().mockResolvedValue({ suggestions: [{ key: "FOO-42", text: "Suggestion" }] }); | ||
renderIssuesRow({ report: { issue_tracker: { type: "Jira", parameters: { url: "https://jira" } } } }) | ||
await userEvent.type(screen.getByLabelText(/Issue identifiers/), 's'); | ||
expect(screen.queryAllByText(/FOO-42: Suggestion/).length).toBe(1) | ||
await userEvent.clear(screen.getByLabelText(/Issue identifiers/).firstChild); | ||
expect(screen.queryAllByText(/FOO-42: Suggestion/).length).toBe(0) | ||
}); |
Oops, something went wrong.