diff --git a/README.md b/README.md index b4cf6726..3d677eda 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,7 @@ You can set your user's settings by clicking in (your user name) -> Settings. Available options: - set default team + - set labels for Jira plugin ### Teams All data is organized by Teams: a team groups a set of repositories to show data more concisely and acts as a global filter. diff --git a/backend/api/server/router/jira/jira.go b/backend/api/server/router/jira/jira.go index 78004d40..faeb9bc9 100644 --- a/backend/api/server/router/jira/jira.go +++ b/backend/api/server/router/jira/jira.go @@ -27,7 +27,7 @@ func (s *jiraRouter) listBugsAffectingCI(ctx context.Context, w http.ResponseWri team, err := s.Storage.GetTeamByName(teamName[0]) if err != nil { - s.Logger.Error("Failed to fetch bugs") + s.Logger.Error("Failed to fetch team") return httputils.WriteJSON(w, http.StatusInternalServerError, &types.ErrorResponse{ Message: err.Error(), diff --git a/frontend/src/app/Github/Github.tsx b/frontend/src/app/Github/Github.tsx index 34319d2a..16023892 100644 --- a/frontend/src/app/Github/Github.tsx +++ b/frontend/src/app/Github/Github.tsx @@ -234,7 +234,7 @@ let GitHub = () => { } - {!loadingState && + {!isInvalid && !loadingState && ( diff --git a/frontend/src/app/Jira/Jira.tsx b/frontend/src/app/Jira/Jira.tsx index 59488b53..3438a49d 100644 --- a/frontend/src/app/Jira/Jira.tsx +++ b/frontend/src/app/Jira/Jira.tsx @@ -11,12 +11,13 @@ import { ToggleGroup, ToggleGroupItem, Spinner, - FormGroup, - Form, - TextInput, + ButtonVariant, Button, Modal, + FormGroup, Popover, + TextInput, + Form, } from '@patternfly/react-core'; import { TableComposable, @@ -28,7 +29,7 @@ import { ThProps } from '@patternfly/react-table'; import { Chart, ChartAxis, ChartGroup, ChartLine, createContainer, ChartThemeColor } from '@patternfly/react-charts'; -import { getJirasResolutionTime, getJirasOpen, listBugsAffectingCI } from '@app/utils/APIService'; +import { getJirasResolutionTime, getJirasOpen, listBugsAffectingCI, createUser } from '@app/utils/APIService'; import { ReactReduxContext, useSelector } from 'react-redux'; import { formatDate, getRangeDates } from '@app/Reports/utils'; import { DateTimeRangePicker } from '@app/utils/DateTimeRangePicker'; @@ -39,7 +40,8 @@ import { CustomStackChart } from './CustomStackChart'; import { ListIssues } from './ListIssuesTable'; import { getIssuesByFields, getIssuesByLabels, getLegend } from './utils'; import { getLabels } from '@app/utils/utils'; -import { HelpIcon } from '@patternfly/react-icons'; +import { UserConfig } from '@app/Teams/User'; +import { CogIcon, HelpIcon, PlusIcon } from '@patternfly/react-icons'; interface Bugs { jira_key: string; @@ -71,10 +73,21 @@ export const Jira = () => { const [rangeDateTime, setRangeDateTime] = useState(getRangeDates(30)); const priorities = ["Global", "Major", "Critical", "Blocker", "Normal", "Undefined", "Minor"] const [loadingState, setLoadingState] = useState(false); - const [labels, setLabels] = useState(defaultLabels); + const [labels, setLabels] = useState([]); const [openIssuesTable, setOpenIssuesTable] = useState({}); const [closedIssuesTable, setClosedIssuesTable] = useState({}); + + useEffect(() => { + if (state.auth.USER_CONFIG != "" && state.auth.USER_CONFIG != undefined) { + let userConfig = JSON.parse(state.auth.USER_CONFIG) as UserConfig; + const userConfigJiraLabels = userConfig.jira_config.labels + if (userConfigJiraLabels != "") { + setLabelsValue(userConfigJiraLabels) + } + } + }, []); + const getJiraData = (ID) => { setLoadingState(true) const newData = {} @@ -107,7 +120,9 @@ export const Jira = () => { } useEffect(() => { - if (currentTeam != "" && currentTeam != undefined) { + const team = params.get('team'); + + if (team == currentTeam || team == null) { listBugsAffectingCI(currentTeam).then(res => { setBugsKnown(res.data) }) @@ -122,16 +137,28 @@ export const Jira = () => { const labelParam = params.get("label_selected") if (start == null || end == null || labelsParam == null || labelParam == null) { - setIsLabelSelected(labels[0]) + let lbs = defaultLabels + + if (state.auth.USER_CONFIG != "" && state.auth.USER_CONFIG != undefined) { + var userConfig = JSON.parse(state.auth.USER_CONFIG) as UserConfig; + if (userConfig.jira_config.labels != "") { + setLabels(userConfig.jira_config.labels.split(",")) + lbs = userConfig.jira_config.labels.split(",") + } + } + + setLabels(lbs) + setIsLabelSelected(lbs[0]) history.push( '/home/jira?team=' + currentTeam + '&selected=' + ID + '&start=' + formatDate(rangeDateTime[0]) + '&end=' + formatDate(rangeDateTime[1]) + - '&labels=' + labels + - '&label_selected=' + labels[0] + '&labels=' + lbs.toString() + + '&label_selected=' + lbs[0] ) + } else { setRangeDateTime([new Date(start), new Date(end)]) const lbls = labelsParam.split(",") @@ -143,7 +170,7 @@ export const Jira = () => { '&selected=' + ID + '&start=' + start + '&end=' + end + - '&labels=' + labels + + '&labels=' + labelsParam + '&label_selected=' + labelParam ) } @@ -164,12 +191,6 @@ export const Jira = () => { const [isSelected, setIsSelected] = React.useState('open'); const [isLabelSelected, setIsLabelSelected] = React.useState(''); - const handleItemClick = (isSelected: boolean, event: React.MouseEvent | React.KeyboardEvent | MouseEvent) => { - const id = event.currentTarget.id; - setBugsTable([]) - setIsSelected(id); - }; - useEffect(() => { const selected = params.get("selected") const target = selected != null ? selected : "Global" @@ -303,9 +324,9 @@ export const Jira = () => { }; const [isModalOpen, setIsModalOpen] = React.useState(false); - const [labelsValue, setLabelsValue] = React.useState(""); + const [labelsValue, setLabelsValue] = React.useState(defaultLabels.join(",")); type validate = 'success' | 'warning' | 'error' | 'default'; - const [labelsValidated, setLabelsValidated] = React.useState('error'); + const [labelsValidated, setLabelsValidated] = React.useState(); const regexp = new RegExp('^[a-zA-Z_-]+(,[0-9a-zA-Z_-]+)*$') @@ -314,6 +335,7 @@ export const Jira = () => { }; const handleLabelsInput = async (value) => { + console.log("value") setLabelsValidated('error'); setLabelsValue(value); if (regexp.test(value)) { @@ -329,9 +351,26 @@ export const Jira = () => { setLabels(ls) setIsLabelSelected(ls[0]) + if (state.auth.USER_CONFIG != "" && state.auth.USER_CONFIG != undefined) { + // get user config + let userConfig = JSON.parse(state.auth.USER_CONFIG) as UserConfig; + userConfig.jira_config.labels = labelsValue + + // get user email + const userClaims = JSON.parse(window.atob(state.auth.IDT.split('.')[1])) + + // update user config + const config = JSON.stringify(userConfig) + createUser(userClaims.email, config) + const redux_dispatch = store.dispatch; + redux_dispatch({ type: "SET_USER_CONFIG", data: config }); + } + + // update params and reload page params.set("labels", labelsValue) params.set("label_selected", ls[0]) history.push(window.location.pathname + '?' + params.toString()); + window.location.reload(); }; return ( @@ -345,12 +384,21 @@ export const Jira = () => { {!loadingState && - handleChange(event, from, to)} - > - + + handleChange(event, from, to)} + > + + + Labels Configuration + + @@ -493,9 +541,6 @@ export const Jira = () => { } - - Configure labels - { } {(openIssuesTable.length > 0) && getIssuesByFields(openIssuesTable, labels, "component")?.filter((x) => { - let exists = false - x.filter((x) => { + let exists = false + x.filter((x) => { if (x.y != 0) { exists = true } }) - return exists + return exists }).length > 0 && @@ -574,7 +619,7 @@ export const Jira = () => { } - + {(openIssuesTable.length > 0) && getIssuesByFields(openIssuesTable, labels, "status").filter((x) => { let exists = false x.filter((x) => { @@ -583,7 +628,7 @@ export const Jira = () => { } }) return exists - }).length > 0 && + }).length > 0 && Open Issues by Labels and Status @@ -624,28 +669,28 @@ export const Jira = () => { } - - List of open issues - - - - - {labels?.map((label, idx) => { - return ( - - ) - })} - - - - - + + List of open issues + + + + + {labels?.map((label, idx) => { + return ( + + ) + })} + + + + + {(closedIssuesTable.length > 0) && diff --git a/frontend/src/app/Login/Login.tsx b/frontend/src/app/Login/Login.tsx index 2bef08ce..288674ec 100644 --- a/frontend/src/app/Login/Login.tsx +++ b/frontend/src/app/Login/Login.tsx @@ -11,6 +11,8 @@ import { ReactReduxContext } from 'react-redux'; import { initOauthFlow, completeOauthFlow, OauthData } from '@app/utils/oauth' import { JwtPayload, jwtDecode } from "jwt-decode"; import { createUser, getUser } from '@app/utils/APIService'; +import { GetUserConfig } from '@app/Teams/User'; +import { defaultLabels } from '@app/Jira/Jira'; let authorizationUrl: URL async function callLogin() { document.location.href = authorizationUrl.toString() @@ -67,12 +69,17 @@ let Login = () => { const sessionInfo = jwtDecode(data.IDT); const user = await getUser(sessionInfo.email) - if (user.data == null) { - await createUser(sessionInfo.email, "") + if (user.data == null) { // user does not exist yet + // get user config + const config = GetUserConfig("n/a", defaultLabels.join(",")) + + // update user config + await createUser(sessionInfo.email, config) + redux_dispatch({ type: "SET_USER_CONFIG", data: config }); } else { redux_dispatch({ type: "SET_USER_CONFIG", data: user.data.config }); } - + setTimeout(function () { setCallbackError("") const API_URL = process.env.REACT_APP_API_SERVER_URL || 'http://localhost:9898' diff --git a/frontend/src/app/Reports/Reports.tsx b/frontend/src/app/Reports/Reports.tsx index a931118b..7226f89e 100644 --- a/frontend/src/app/Reports/Reports.tsx +++ b/frontend/src/app/Reports/Reports.tsx @@ -169,7 +169,6 @@ let Reports = () => { // Called onChange of the repository dropdown element. This set repository name and organization state variables, or clears them when placeholder is selected const setRepoNameOnChange = (event, selection, isPlaceholder) => { - console.log(selection, repositories[selection]) if (isPlaceholder) { clearRepo() } diff --git a/frontend/src/app/Teams/User.tsx b/frontend/src/app/Teams/User.tsx index 0445d5f5..03bcfd66 100644 --- a/frontend/src/app/Teams/User.tsx +++ b/frontend/src/app/Teams/User.tsx @@ -1,23 +1,32 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { createUser, listUsers } from "@app/utils/APIService"; +import { defaultLabels } from "@app/Jira/Jira"; +import { createUser } from "@app/utils/APIService"; import { Button, Dropdown, DropdownGroup, DropdownItem, DropdownSeparator, DropdownToggle, Form, FormGroup, FormSection, FormSelect, FormSelectOption, Modal, ModalVariant, Popover, TextInput, ToolbarGroup, ToolbarItem } from "@patternfly/react-core" -import { CaretDownIcon, CogIcon, DisconnectedIcon, UserCircleIcon } from "@patternfly/react-icons"; +import { CaretDownIcon, CogIcon, DisconnectedIcon, HelpIcon, UserCircleIcon } from "@patternfly/react-icons"; import React, { useEffect, useState } from "react" import { ReactReduxContext, useSelector } from "react-redux"; import { useHistory } from "react-router-dom"; -export interface TeamsConfiguration { +export interface TeamsConfig { default_team: string; } +export interface JiraConfig { + labels: string; +} + export interface UserConfig { - teams_configuration: TeamsConfiguration; + teams_configuration: TeamsConfig; + jira_config: JiraConfig; } -const getUserConfig = (defaultTeam) => { +export const GetUserConfig = (defaultTeam: string, labels: string) => { const cfg: UserConfig = { teams_configuration: { default_team: defaultTeam, + }, + jira_config: { + labels: labels, } }; const config = JSON.stringify(cfg); @@ -36,15 +45,22 @@ export const UserToolbarGroup = () => { const [defaultTeam, setDefaultTeam] = React.useState(''); const currentTeamsAvailable = useSelector((state: any) => state.teams.TeamsAvailable); const redux_dispatch = store.dispatch; + type validate = 'success' | 'warning' | 'error' | 'default'; + const [labelsValidated, setLabelsValidated] = React.useState(); + const [labelsValue, setLabelsValue] = React.useState(""); + const regexp = new RegExp('^[a-zA-Z_-]+(,[0-9a-zA-Z_-]+)*$') const confirm = async () => { // get user config - const config = getUserConfig(defaultTeam) + const config = GetUserConfig(defaultTeam, labelsValue) // update user config await createUser(userEmail, config) redux_dispatch({ type: "SET_USER_CONFIG", data: config }); setIsModalOpen(!isModalOpen); + + const params = new URLSearchParams(window.location.search); + history.push(window.location.pathname + '?' + 'team=' + params.get('team')); window.location.reload(); }; @@ -60,16 +76,19 @@ export const UserToolbarGroup = () => { setDropdownOpen(isDropdownOpen); } - listUsers() - useEffect(() => { setDefaultTeam("n/a") + setLabelsValue(defaultLabels.join(",")) if (state.auth.USER_CONFIG != "" && state.auth.USER_CONFIG != undefined) { let userConfig = JSON.parse(state.auth.USER_CONFIG) as UserConfig; const userConfigDefaultTeam = userConfig.teams_configuration.default_team if (userConfigDefaultTeam != "") { setDefaultTeam(userConfigDefaultTeam) } + const userConfigJiraLabels = userConfig.jira_config.labels + if (userConfigJiraLabels != "") { + setLabelsValue(userConfigJiraLabels) + } } try { @@ -92,6 +111,15 @@ export const UserToolbarGroup = () => { setIsModalOpen(!isModalOpen); }; + const handleLabelsInput = async (value) => { + setLabelsValue(value); + if (regexp.test(value)) { + setLabelsValidated('success'); + } else { + setLabelsValidated('error'); + } + }; + const UserDropDownItems = [ @@ -108,49 +136,73 @@ export const UserToolbarGroup = () => { ]; - const UserModal = () => { - return ( - - - Confirm - , - - Cancel - - ]} - ouiaId="BasicModal" - > - - - - - {currentTeamsAvailable.map((option, index) => ( - - ))} - - - - - - - ) - } - return ( - + + Confirm + , + + Cancel + + ]} + ouiaId="BasicModal" + > + + + + + {currentTeamsAvailable.map((option, index) => ( + + ))} + + + + + + } bodyContent={Add a list of labels separated by comma. Example: all,test_bug,product_bug,to_investigate}> + e.preventDefault()} + aria-describedby="modal-with-form-form-name" + className="pf-c-form__group-label-help" + > + + + + } + isRequired + fieldId="modal-with-form-form-name" + helperTextInvalid="Must be a valid JIRA key" + > + + + + + { ) } + diff --git a/frontend/src/app/utils/utils.ts b/frontend/src/app/utils/utils.ts index 490502f1..ee54a0ed 100644 --- a/frontend/src/app/utils/utils.ts +++ b/frontend/src/app/utils/utils.ts @@ -66,7 +66,7 @@ export function validateRepositoryParams(repos, repository, organization) { // validateParam validates if 'param' exists in 'params' export function validateParam(params, param) { - if (params.find((p) => p == param)) { + if (params.find((p) => p.team_name == param)) { return true; } return false;