From d3fe654e2395e30cbd4b4aa30e94dcb18418c0b3 Mon Sep 17 00:00:00 2001 From: Ritik Kapoor Date: Tue, 12 Dec 2023 18:26:25 +0000 Subject: [PATCH] [CODE-1144] UI : Added author filter on PR page (#855) --- web/src/pages/PullRequests/PullRequests.tsx | 11 ++- .../PullRequestsContentHeader.tsx | 73 ++++++++++++++++++- web/src/services/config.ts | 65 +++++++++++++++++ 3 files changed, 142 insertions(+), 7 deletions(-) diff --git a/web/src/pages/PullRequests/PullRequests.tsx b/web/src/pages/PullRequests/PullRequests.tsx index 8647c55ab..74510a1d1 100644 --- a/web/src/pages/PullRequests/PullRequests.tsx +++ b/web/src/pages/PullRequests/PullRequests.tsx @@ -56,19 +56,18 @@ export default function PullRequests() { UserPreference.PULL_REQUESTS_FILTER_SELECTED_OPTIONS, PullRequestFilterOption.OPEN ) + const [authorFilter, setAuthorFilter] = useState() const space = useGetSpaceParam() const { updateQueryParams } = useUpdateQueryParams() const pageBrowser = useQueryParams() const pageInit = pageBrowser.page ? parseInt(pageBrowser.page) : 1 const [page, setPage] = usePageIndex(pageInit) - useEffect(() => { if (page > 1) { updateQueryParams({ page: page.toString() }) } }, [setPage]) // eslint-disable-line react-hooks/exhaustive-deps - const { repoMetadata, error, loading, refetch } = useGetRepositoryMetadata() const { data, @@ -84,7 +83,8 @@ export default function PullRequests() { sort: filter == PullRequestFilterOption.MERGED ? 'merged' : 'number', order: 'desc', query: searchTerm, - state: filter == PullRequestFilterOption.ALL ? '' : filter + state: filter == PullRequestFilterOption.ALL ? '' : filter, + ...(authorFilter && { created_by: Number(authorFilter) }) }, debounce: 500, lazy: !repoMetadata @@ -234,6 +234,11 @@ export default function PullRequests() { setSearchTerm(value) setPage(1) }} + activePullRequestAuthorFilterOption={authorFilter} + onPullRequestAuthorFilterChanged={_authorFilter => { + setAuthorFilter(_authorFilter) + setPage(1) + }} /> diff --git a/web/src/pages/PullRequests/PullRequestsContentHeader/PullRequestsContentHeader.tsx b/web/src/pages/PullRequests/PullRequestsContentHeader/PullRequestsContentHeader.tsx index 2e70e1ab1..4adcc029d 100644 --- a/web/src/pages/PullRequests/PullRequestsContentHeader/PullRequestsContentHeader.tsx +++ b/web/src/pages/PullRequests/PullRequestsContentHeader/PullRequestsContentHeader.tsx @@ -16,11 +16,14 @@ import { useHistory } from 'react-router-dom' import React, { useMemo, useState } from 'react' -import { Container, Layout, FlexExpander, DropDown, ButtonVariation, Button } from '@harnessio/uicore' +import { Container, Layout, FlexExpander, DropDown, ButtonVariation, Button, SelectOption } from '@harnessio/uicore' +import { sortBy } from 'lodash-es' +import { getConfig, getUsingFetch } from 'services/config' import { useStrings } from 'framework/strings' import { CodeIcon, GitInfoProps, makeDiffRefs, PullRequestFilterOption } from 'utils/GitUtils' import { UserPreference, useUserPreference } from 'hooks/useUserPreference' import { useGetSpaceParam } from 'hooks/useGetSpaceParam' +import type { TypesPrincipalInfo } from 'services/code' import { useAppContext } from 'AppContext' import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner' import { permissionProps } from 'utils/Utils' @@ -29,15 +32,19 @@ import css from './PullRequestsContentHeader.module.scss' interface PullRequestsContentHeaderProps extends Pick { loading?: boolean activePullRequestFilterOption?: string + activePullRequestAuthorFilterOption?: string onPullRequestFilterChanged: (filter: string) => void + onPullRequestAuthorFilterChanged: (authorFilter: string) => void onSearchTermChanged: (searchTerm: string) => void } export function PullRequestsContentHeader({ loading, onPullRequestFilterChanged, + onPullRequestAuthorFilterChanged, onSearchTermChanged, activePullRequestFilterOption = PullRequestFilterOption.OPEN, + activePullRequestAuthorFilterOption, repoMetadata }: PullRequestsContentHeaderProps) { const history = useHistory() @@ -47,10 +54,13 @@ export function PullRequestsContentHeader({ UserPreference.PULL_REQUESTS_FILTER_SELECTED_OPTIONS, activePullRequestFilterOption ) - const [searchTerm, setSearchTerm] = useState('') - const space = useGetSpaceParam() - const { standalone } = useAppContext() + const [authorFilterOption, setAuthorFilterOption] = useState(activePullRequestAuthorFilterOption) + const [searchTerm, setSearchTerm] = useState('') + const [query, setQuery] = useState('') + const [loadingAuthors, setLoadingAuthors] = useState(false) + const space = useGetSpaceParam() + const { standalone, routingId } = useAppContext() const { hooks } = useAppContext() const permPushResult = hooks?.usePermissionTranslate?.( { @@ -73,6 +83,40 @@ export function PullRequestsContentHeader({ [getString] ) + const getAuthorsPromise = (): Promise => { + return new Promise((resolve, reject) => { + setLoadingAuthors(true) + try { + getUsingFetch(getConfig('code/api/v1'), `/principals`, { + queryParams: { + query: query?.trim(), + type: 'user', + accountIdentifier: routingId + } + }) + .then((obj: TypesPrincipalInfo[]) => { + const updatedAuthorsList = Array.isArray(obj) + ? ([ + ...(obj || []).map(item => ({ + label: String(item?.display_name), + value: String(item?.id) + })) + ] as SelectOption[]) + : ([] as SelectOption[]) + setLoadingAuthors(false) + resolve(sortBy(updatedAuthorsList, item => item.label.toLowerCase())) + }) + .catch(error => { + setLoadingAuthors(false) + reject(error) + }) + } catch (error) { + setLoadingAuthors(false) + reject(error) + } + }) + } + return ( @@ -86,6 +130,27 @@ export function PullRequestsContentHeader({ }} /> + getAuthorsPromise()} + disabled={loadingAuthors} + onChange={({ value, label }) => { + setAuthorFilterOption(label as string) + onPullRequestAuthorFilterChanged(value as string) + }} + popoverClassName={css.branchDropdown} + icon="nav-user-profile" + iconProps={{ size: 16 }} + placeholder="Select Authors" + addClearBtn={true} + resetOnClose + resetOnSelect + resetOnQuery + query={query} + onQueryChange={newQuery => { + setQuery(newQuery) + }} + /> { // 'code/api/v1' -> 'api/v1' (standalone) // -> 'code/api/v1' (embedded inside Harness platform) @@ -23,3 +25,66 @@ export const getConfig = (str: string): string => { return window.apiUrl ? `${window.apiUrl}/${str}` : `${window.harnessNameSpace || ''}/${str}` } + +export interface GetUsingFetchProps< + _TData = any, + _TError = any, + TQueryParams = { + [key: string]: any + }, + TPathParams = { + [key: string]: any + } +> { + queryParams?: TQueryParams + queryParamStringifyOptions?: IStringifyOptions + pathParams?: TPathParams + requestOptions?: RequestInit + mock?: _TData +} + +export const getUsingFetch = < + TData = any, + _TError = any, + TQueryParams = { + [key: string]: any + }, + TPathParams = { + [key: string]: any + } +>( + base: string, + path: string, + props: GetUsingFetchProps, + signal?: RequestInit['signal'] +): Promise => { + if (props.mock) return Promise.resolve(props.mock) + let url = base + path + if (props.queryParams && Object.keys(props.queryParams).length) { + url += `?${qs.stringify(props.queryParams, props.queryParamStringifyOptions)}` + } + return fetch(url, { + signal, + ...(props.requestOptions || {}) + // headers: getHeaders(props.requestOptions?.headers) + }).then(res => { + // custom event to allow the app framework to handle api responses + const responseEvent = new CustomEvent('PROMISE_API_RESPONSE', { detail: { response: res } }) + window.dispatchEvent(responseEvent) // this will be captured in App.tsx to handle 401 and token refresh + + const contentType = res.headers.get('content-type') || '' + + if (contentType.toLowerCase().indexOf('application/json') > -1) { + if (res.status === 401) { + return res.json().then(json => Promise.reject(json)) + } + return res.json() + } + + if (res.status === 401) { + return res.text().then(text => Promise.reject(text)) + } + + return res.text() + }) +}