clean up FilteredConnection filter types and code (#63590)

- Renames some types and fields for clarity:
  - FilteredConnectionFilter -> Filter
    - field name `values` -> `options`
  - FilteredConnectionFilterValue -> FilterOption
- Avoids passing around unnecessary data

No behavior change. This makes it easier to use the FilterControl
component.

## Test plan

Use existing filtered connections with filters (see the diff for a list
of pages that contain this component).
This commit is contained in:
Quinn Slack 2024-07-03 00:13:17 -07:00 committed by GitHub
parent 121a01beb6
commit a73b1b0964
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 232 additions and 254 deletions

View File

@ -6,7 +6,41 @@ import { RadioButtons } from '../RadioButtons'
import styles from './FilterControl.module.scss'
export interface FilteredConnectionFilterValue {
/**
* A filter to display next to the search input field.
*/
export interface Filter {
/** The UI label for the filter. */
label: string
/** The UI form control to use when displaying this filter. */
type: 'radio' | 'select'
/**
* The URL query parameter name for this filter (conventionally the label, lowercased and
* without spaces and punctuation).
*/
id: string
/** An optional tooltip to display for this filter. */
tooltip?: string
/**
* All of the possible values for this filter that the user can select.
*/
options: FilterOption[]
}
/**
* An option that the user can select for a filter ({@link Filter}).
*/
export interface FilterOption {
/**
* The value (corresponding to the key in {@link Filter.id}) if this option is chosen. For
* example, if a filter has {@link Filter.id} of `sort` and the user selects a
* {@link FilterOption} with {@link FilterOption.value} of `asc`, then the URL query string
* would be `sort=asc`.
*/
value: string
label: string
tooltip?: string
@ -14,34 +48,18 @@ export interface FilteredConnectionFilterValue {
}
/**
* A filter to display next to the search input field.
* The values of all filters, keyed by the filter ID ({@link Filter.id}).
*/
export interface FilteredConnectionFilter {
/** The UI label for the filter. */
label: string
/** "radio" or "select" */
type: string
/**
* The URL string for this filter (conventionally the label, lowercased and without spaces and punctuation).
*/
id: string
/** An optional tooltip to display for this filter. */
tooltip?: string
values: FilteredConnectionFilterValue[]
}
export interface FilterValues extends Record<string, FilterOption['value'] | null> {}
interface FilterControlProps {
/** All filters. */
filters: FilteredConnectionFilter[]
filters: Filter[]
/** Called when a filter is selected. */
onValueSelect: (filter: FilteredConnectionFilter, value: FilteredConnectionFilterValue) => void
onValueSelect: (filter: Filter, value: FilterOption['value']) => void
values: Map<string, FilteredConnectionFilterValue>
values: FilterValues
}
export const FilterControl: React.FunctionComponent<React.PropsWithChildren<FilterControlProps>> = ({
@ -51,12 +69,12 @@ export const FilterControl: React.FunctionComponent<React.PropsWithChildren<Filt
children,
}) => {
const onChange = useCallback(
(filter: FilteredConnectionFilter, id: string) => {
const value = filter.values.find(value => value.value === id)
(filter: Filter, id: string) => {
const value = filter.options.find(opt => opt.value === id)
if (value === undefined) {
return
}
onValueSelect(filter, value)
onValueSelect(filter, value.value)
},
[onValueSelect]
)
@ -70,8 +88,8 @@ export const FilterControl: React.FunctionComponent<React.PropsWithChildren<Filt
key={filter.id}
name={filter.id}
className="d-inline-flex flex-row"
selected={values.get(filter.id)?.value}
nodes={filter.values.map(({ value, label, tooltip }) => ({
selected={values[filter.id] ?? undefined}
nodes={filter.options.map(({ value, label, tooltip }) => ({
tooltip,
label,
id: value,
@ -94,12 +112,12 @@ export const FilterControl: React.FunctionComponent<React.PropsWithChildren<Filt
id=""
name={filter.id}
onChange={event => onChange(filter, event.currentTarget.value)}
value={values.get(filter.id)?.value}
value={values[filter.id] ?? undefined}
className="mb-0"
isCustomStyle={true}
>
{filter.values.map(value => (
<option key={value.value} value={value.value} label={value.label} />
{filter.options.map(opt => (
<option key={opt.value} value={opt.value} label={opt.label} />
))}
</Select>
</div>

View File

@ -2,8 +2,8 @@ import * as React from 'react'
import type * as H from 'history'
import { isEqual, uniq } from 'lodash'
import { type NavigateFunction, useLocation, useNavigate } from 'react-router-dom'
import { combineLatest, merge, type Observable, of, Subject, Subscription } from 'rxjs'
import { useLocation, useNavigate, type NavigateFunction } from 'react-router-dom'
import { combineLatest, merge, of, Subject, Subscription, type Observable } from 'rxjs'
import {
catchError,
debounceTime,
@ -20,7 +20,7 @@ import {
tap,
} from 'rxjs/operators'
import { asError, type ErrorLike, isErrorLike, logger } from '@sourcegraph/common'
import { asError, isErrorLike, logger, type ErrorLike } from '@sourcegraph/common'
import {
ConnectionNodes,
@ -30,7 +30,7 @@ import {
} from './ConnectionNodes'
import type { Connection, ConnectionQueryArguments } from './ConnectionType'
import { QUERY_KEY } from './constants'
import type { FilteredConnectionFilter, FilteredConnectionFilterValue } from './FilterControl'
import type { Filter, FilterOption, FilterValues } from './FilterControl'
import { ConnectionContainer, ConnectionError, ConnectionForm, ConnectionLoading } from './ui'
import type { ConnectionFormProps } from './ui/ConnectionForm'
import { getFilterFromURL, getUrlQuery, hasID, parseQueryInt } from './utils'
@ -101,7 +101,6 @@ interface FilteredConnectionDisplayProps extends ConnectionNodesDisplayProps, Co
/**
* Props for the FilteredConnection component.
*
* @template C The GraphQL connection type, such as GQL.IRepositoryConnection.
* @template N The node type of the GraphQL connection, such as GQL.IRepository (if C is GQL.IRepositoryConnection)
* @template NP Props passed to `nodeComponent` in addition to `{ node: N }`
@ -131,7 +130,7 @@ interface FilteredConnectionProps<C extends Connection<N>, N, NP = {}, HP = {}>
export interface FilteredConnectionQueryArguments extends ConnectionQueryArguments {}
interface FilteredConnectionState<C extends Connection<N>, N> extends ConnectionNodesState {
activeFilterValues: Map<string, FilteredConnectionFilterValue>
activeFilterValues: FilterValues
/** The fetched connection data or an error (if an error occurred). */
connectionOrError?: C | ErrorLike
@ -161,7 +160,6 @@ interface FilteredConnectionState<C extends Connection<N>, N> extends Connection
* Displays a collection of items with filtering and pagination. It is called
* "connection" because it is intended for use with GraphQL, which calls it that
* (see http://graphql.org/learn/pagination/).
*
* @template N The node type of the GraphQL connection, such as `GQL.IRepository` (if `C` is `GQL.IRepositoryConnection`)
* @template NP Props passed to `nodeComponent` in addition to `{ node: N }`
* @template HP Props passed to `headComponent` in addition to `{ nodes: N[]; totalCount?: number | null }`.
@ -186,7 +184,7 @@ class InnerFilteredConnection<N, NP = {}, HP = {}, C extends Connection<N> = Con
}
private queryInputChanges = new Subject<string>()
private activeFilterValuesChanges = new Subject<Map<string, FilteredConnectionFilterValue>>()
private activeFilterValuesChanges = new Subject<FilterValues>()
private showMoreClicks = new Subject<void>()
private componentUpdates = new Subject<FilteredConnectionProps<C, N, NP, HP>>()
private subscriptions = new Subscription()
@ -214,8 +212,7 @@ class InnerFilteredConnection<N, NP = {}, HP = {}, C extends Connection<N> = Con
loading: true,
query: (!this.props.hideSearch && this.props.useURLQuery && searchParameters.get(QUERY_KEY)) || '',
activeFilterValues:
(this.props.useURLQuery && getFilterFromURL(searchParameters, this.props.filters)) ||
new Map<string, FilteredConnectionFilterValue>(),
(this.props.useURLQuery && getFilterFromURL(searchParameters, this.props.filters)) || {},
first: (this.props.useURLQuery && parseQueryInt(searchParameters, 'first')) || this.props.defaultFirst!,
visible: (this.props.useURLQuery && parseQueryInt(searchParameters, 'visible')) || 0,
}
@ -248,7 +245,7 @@ class InnerFilteredConnection<N, NP = {}, HP = {}, C extends Connection<N> = Con
}
for (const filter of this.props.filters) {
if (this.props.onFilterSelect) {
const value = values.get(filter.id)
const value = values[filter.id]
if (value === undefined) {
continue
}
@ -264,7 +261,7 @@ class InnerFilteredConnection<N, NP = {}, HP = {}, C extends Connection<N> = Con
// Use this.activeFilterChanges not activeFilterChanges so that it doesn't trigger on the initial mount
// (it doesn't need to).
this.activeFilterValuesChanges.subscribe(values => {
this.setState({ activeFilterValues: new Map(values) })
this.setState({ activeFilterValues: values })
})
)
@ -277,10 +274,10 @@ class InnerFilteredConnection<N, NP = {}, HP = {}, C extends Connection<N> = Con
.pipe(
// Track whether the query or the active order or filter changed
scan<
[string, Map<string, FilteredConnectionFilterValue> | undefined, { forceRefresh: boolean }],
[string, FilterValues | undefined, { forceRefresh: boolean }],
{
query: string
filterValues: Map<string, FilteredConnectionFilterValue> | undefined
filterValues: FilterValues | undefined
shouldRefresh: boolean
queryCount: number
}
@ -311,7 +308,9 @@ class InnerFilteredConnection<N, NP = {}, HP = {}, C extends Connection<N> = Con
first: (queryCount === 1 && this.state.visible) || this.state.first,
after: shouldRefresh ? undefined : this.state.after,
query,
...(filterValues ? this.buildArgs(filterValues) : {}),
...(this.props.filters && filterValues
? this.buildArgs(this.props.filters, filterValues)
: {}),
})
.pipe(
catchError(error => [asError(error)]),
@ -406,7 +405,7 @@ class InnerFilteredConnection<N, NP = {}, HP = {}, C extends Connection<N> = Con
this.props.onUpdate(
connectionOrError,
this.state.query,
this.buildArgs(this.state.activeFilterValues)
this.buildArgs(this.props.filters ?? [], this.state.activeFilterValues)
)
}
this.setState({ connectionOrError, ...rest })
@ -511,7 +510,7 @@ class InnerFilteredConnection<N, NP = {}, HP = {}, C extends Connection<N> = Con
}: {
first?: number
query?: string
filterValues?: Map<string, FilteredConnectionFilterValue>
filterValues?: FilterValues
visibleResultCount?: number
}): string {
if (!first) {
@ -647,13 +646,11 @@ class InnerFilteredConnection<N, NP = {}, HP = {}, C extends Connection<N> = Con
this.queryInputChanges.next(event.currentTarget.value)
}
private onDidSelectFilterValue = (filter: FilteredConnectionFilter, value: FilteredConnectionFilterValue): void => {
private onDidSelectFilterValue = (filter: Filter, value: FilterOption['value'] | null): void => {
if (this.props.filters === undefined) {
return
}
const values = new Map(this.state.activeFilterValues)
values.set(filter.id, value)
this.activeFilterValuesChanges.next(values)
this.activeFilterValuesChanges.next({ ...this.state.activeFilterValues, [filter.id]: value })
}
private onClickShowMore = (): void => {
@ -663,21 +660,23 @@ class InnerFilteredConnection<N, NP = {}, HP = {}, C extends Connection<N> = Con
private buildArgs = buildFilterArgs
}
export const buildFilterArgs = (filterValues: Map<string, FilteredConnectionFilterValue>): FilteredConnectionArgs => {
export const buildFilterArgs = (filters: Filter[], filterValues: FilterValues): FilteredConnectionArgs => {
let args: FilteredConnectionArgs = {}
for (const key of filterValues.keys()) {
const value = filterValues.get(key)
for (const [filterID, value] of Object.entries(filterValues)) {
if (value === undefined) {
continue
}
args = { ...args, ...value.args }
const filter = filters.find(f => f.id === filterID)
if (filter) {
const valueArgs = filter.options.find(opt => opt.value === value)?.args
args = { ...args, ...valueArgs }
}
}
return args
}
/**
* Resets the `FilteredConnection` URL query string parameters to the defaults
*
* @param parameters the current URL search parameters
*/
export const resetFilteredConnectionURLQuery = (parameters: URLSearchParams): void => {

View File

@ -3,9 +3,9 @@ import React, { useCallback, useRef } from 'react'
import classNames from 'classnames'
import { useMergeRefs } from 'use-callback-ref'
import { useAutoFocus, Input, Form } from '@sourcegraph/wildcard'
import { Form, Input, useAutoFocus } from '@sourcegraph/wildcard'
import { FilterControl, type FilteredConnectionFilter, type FilteredConnectionFilterValue } from '../FilterControl'
import { FilterControl, type Filter, type FilterOption, type FilterValues } from '../FilterControl'
import styles from './ConnectionForm.module.scss'
@ -42,14 +42,14 @@ export interface ConnectionFormProps {
*
* Filters are mutually exclusive.
*/
filters?: FilteredConnectionFilter[]
filters?: Filter[]
onFilterSelect?: (filter: FilteredConnectionFilter, value: FilteredConnectionFilterValue) => void
onFilterSelect?: (filter: Filter, value: FilterOption['value'] | null) => void
/** An element rendered as a sibling of the filters. */
additionalFilterElement?: React.ReactElement
filterValues?: Map<string, FilteredConnectionFilterValue>
filterValues?: FilterValues
compact?: boolean
}

View File

@ -7,7 +7,7 @@ import type { Scalars } from '@sourcegraph/shared/src/graphql-operations'
import type { Connection } from './ConnectionType'
import { QUERY_KEY } from './constants'
import type { FilteredConnectionFilter, FilteredConnectionFilterValue } from './FilterControl'
import type { Filter, FilterValues } from './FilterControl'
/** Checks if the passed value satisfies the GraphQL Node interface */
export const hasID = (value: unknown): value is { id: Scalars['ID'] } =>
@ -19,26 +19,22 @@ export const hasDisplayName = (value: unknown): value is { displayName: Scalars[
hasProperty('displayName')(value) &&
typeof value.displayName === 'string'
export const getFilterFromURL = (
searchParameters: URLSearchParams,
filters: FilteredConnectionFilter[] | undefined
): Map<string, FilteredConnectionFilterValue> => {
const values: Map<string, FilteredConnectionFilterValue> = new Map<string, FilteredConnectionFilterValue>()
if (filters === undefined || filters.length === 0) {
export const getFilterFromURL = (searchParameters: URLSearchParams, filters: Filter[] | undefined): FilterValues => {
const values: FilterValues = {}
if (filters === undefined) {
return values
}
for (const filter of filters) {
const urlValue = searchParameters.get(filter.id)
if (urlValue !== null) {
const value = filter.values.find(value => value.value === urlValue)
const value = filter.options.find(opt => opt.value === urlValue)
if (value !== undefined) {
values.set(filter.id, value)
values[filter.id] = value.value
continue
}
}
// couldn't find a value, add default
values.set(filter.id, filter.values[0])
values[filter.id] = filter.options[0].value
}
return values
}
@ -70,8 +66,8 @@ export interface GetUrlQueryParameters {
default: number
}
query?: string
filterValues?: Map<string, FilteredConnectionFilterValue>
filters?: FilteredConnectionFilter[]
filterValues?: FilterValues
filters?: Filter[]
visibleResultCount?: number
search: Location['search']
}
@ -99,12 +95,12 @@ export const getUrlQuery = ({
if (filterValues && filters) {
for (const filter of filters) {
const value = filterValues.get(filter.id)
if (value === undefined) {
const value = filterValues[filter.id]
if (value === undefined || value === null) {
continue
}
if (value !== filter.values[0]) {
searchParameters.set(filter.id, value.value)
if (value !== filter.options[0].value) {
searchParameters.set(filter.id, value)
} else {
searchParameters.delete(filter.id)
}

View File

@ -1,11 +1,11 @@
import { type FC, useEffect } from 'react'
import { useEffect, type FC } from 'react'
import { mdiPlus } from '@mdi/js'
import { Navigate, useLocation } from 'react-router-dom'
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { Link, ButtonLink, Icon, PageHeader, Container } from '@sourcegraph/wildcard'
import { ButtonLink, Container, Icon, Link, PageHeader } from '@sourcegraph/wildcard'
import {
ConnectionContainer,

View File

@ -167,7 +167,7 @@ export const CodeIntelConfigurationPage: FunctionComponent<CodeIntelConfiguratio
id: 'filters',
label: 'Show',
type: 'select',
values: [
options: [
{
label: 'All policies',
value: 'all',

View File

@ -1,4 +1,4 @@
import { type FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useState, type FunctionComponent } from 'react'
import { useApolloClient } from '@apollo/client'
import { mdiChevronRight, mdiDelete, mdiMapSearch, mdiRedo } from '@mdi/js'
@ -29,16 +29,16 @@ import {
import {
FilteredConnection,
type FilteredConnectionFilter,
type Filter,
type FilteredConnectionQueryArguments,
} from '../../../../components/FilteredConnection'
import { PageTitle } from '../../../../components/PageTitle'
import {
PreciseIndexState,
type IndexerListResult,
type IndexerListVariables,
type PreciseIndexesVariables,
type PreciseIndexFields,
PreciseIndexState,
type PreciseIndexesVariables,
} from '../../../../graphql-operations'
import { FlashMessage } from '../../configuration/components/FlashMessage'
import { PreciseIndexLastUpdated } from '../components/CodeIntelLastUpdated'
@ -71,11 +71,11 @@ export interface CodeIntelPreciseIndexesPageProps extends TelemetryProps, Teleme
indexingEnabled?: boolean
}
const STATE_FILTER: FilteredConnectionFilter = {
const STATE_FILTER: Filter = {
id: 'filters',
label: 'State',
type: 'select',
values: [
options: [
{
label: 'All',
value: 'all',
@ -146,12 +146,12 @@ export const CodeIntelPreciseIndexesPage: FunctionComponent<CodeIntelPreciseInde
const { data: indexerData } = useQuery<IndexerListResult, IndexerListVariables>(INDEXER_LIST, {})
const filters = useMemo<FilteredConnectionFilter[]>(() => {
const indexerFilter: FilteredConnectionFilter = {
const filters = useMemo<Filter[]>(() => {
const indexerFilter: Filter = {
id: 'filters-indexer',
label: 'Indexer',
type: 'select',
values: [
options: [
{
label: 'All',
value: 'all',
@ -163,7 +163,7 @@ export const CodeIntelPreciseIndexesPage: FunctionComponent<CodeIntelPreciseInde
const keys = (indexerData?.indexerKeys || []).filter(key => Boolean(key))
for (const key of keys) {
indexerFilter.values.push({
indexerFilter.options.push({
label: key,
value: key,
args: { indexerKey: key },

View File

@ -5,11 +5,11 @@ import { mdiMapSearch } from '@mdi/js'
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import { EVENT_LOGGER } from '@sourcegraph/shared/src/telemetry/web/eventLogger'
import { Container, Link, PageHeader, Icon, H3, Text } from '@sourcegraph/wildcard'
import { Container, H3, Icon, Link, PageHeader, Text } from '@sourcegraph/wildcard'
import {
FilteredConnection,
type FilteredConnectionFilter,
type Filter,
type FilteredConnectionQueryArguments,
} from '../../../components/FilteredConnection'
import { PageTitle } from '../../../components/PageTitle'
@ -18,12 +18,12 @@ import type { ExecutorFields } from '../../../graphql-operations'
import { ExecutorNode } from './ExecutorNode'
import { queryExecutors as defaultQueryExecutors } from './useExecutors'
const filters: FilteredConnectionFilter[] = [
const filters: Filter[] = [
{
id: 'filters',
label: 'State',
type: 'select',
values: [
options: [
{
label: 'All',
value: 'all',

View File

@ -17,8 +17,8 @@ import type { AuthenticatedUser } from '../../auth'
import {
FilteredConnection,
type Connection,
type FilteredConnectionFilter,
type FilteredConnectionFilterValue,
type Filter,
type FilterOption,
} from '../../components/FilteredConnection'
import { useDefaultContext } from './hooks/useDefaultContext'
@ -64,7 +64,7 @@ export const SearchContextsList: React.FunctionComponent<SearchContextsListProps
[authenticatedUser, fetchSearchContexts, getUserSearchContextNamespaces, platformContext]
)
const ownerNamespaceFilterValues: FilteredConnectionFilterValue[] = useMemo(
const ownerNamespaceFilterValues: FilterOption[] = useMemo(
() =>
authenticatedUser
? [
@ -87,14 +87,14 @@ export const SearchContextsList: React.FunctionComponent<SearchContextsListProps
[authenticatedUser]
)
const filters: FilteredConnectionFilter[] = useMemo(
const filters = useMemo<Filter[]>(
() => [
{
label: 'Sort',
type: 'select',
id: 'order',
tooltip: 'Order search contexts',
values: [
options: [
{
value: 'spec-asc',
label: 'By name',
@ -118,7 +118,7 @@ export const SearchContextsList: React.FunctionComponent<SearchContextsListProps
type: 'select',
id: 'owner',
tooltip: 'Search context owner',
values: [
options: [
{
value: 'all',
label: 'All',

View File

@ -1,9 +1,9 @@
import { type FC, useCallback, useEffect } from 'react'
import { useCallback, useEffect, type FC } from 'react'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { H2 } from '@sourcegraph/wildcard'
import { FilteredConnection, type FilteredConnectionFilter } from '../../components/FilteredConnection'
import { FilteredConnection, type Filter } from '../../components/FilteredConnection'
import type {
ListNotebooksResult,
ListNotebooksVariables,
@ -20,7 +20,7 @@ import styles from './NotebooksList.module.scss'
export interface NotebooksListProps extends TelemetryProps {
title: string
logEventName: NotebooksFilterEvents
orderOptions: FilteredConnectionFilter[]
orderOptions: Filter[]
creatorUserID?: string
starredByUserID?: string
namespace?: string

View File

@ -2,22 +2,22 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { mdiBookOutline } from '@mdi/js'
import classNames from 'classnames'
import { type Location, Navigate, useNavigate, useLocation, type NavigateFunction } from 'react-router-dom'
import { Navigate, useLocation, useNavigate, type Location, type NavigateFunction } from 'react-router-dom'
import type { Observable } from 'rxjs'
import { catchError, startWith, switchMap } from 'rxjs/operators'
import { asError, type ErrorLike, isErrorLike } from '@sourcegraph/common'
import { asError, isErrorLike, type ErrorLike } from '@sourcegraph/common'
import { useTemporarySetting } from '@sourcegraph/shared/src/settings/temporary/useTemporarySetting'
import { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { PageHeader, Button, useEventObservable, Alert, ButtonLink } from '@sourcegraph/wildcard'
import { Alert, Button, ButtonLink, PageHeader, useEventObservable } from '@sourcegraph/wildcard'
import type { AuthenticatedUser } from '../../auth'
import type { FilteredConnectionFilter } from '../../components/FilteredConnection'
import type { Filter } from '../../components/FilteredConnection'
import { Page } from '../../components/Page'
import { type CreateNotebookVariables, NotebooksOrderBy } from '../../graphql-operations'
import { NotebooksOrderBy, type CreateNotebookVariables } from '../../graphql-operations'
import { PageRoutes } from '../../routes.constants'
import { fetchNotebooks as _fetchNotebooks, createNotebook as _createNotebook } from '../backend'
import { createNotebook as _createNotebook, fetchNotebooks as _fetchNotebooks } from '../backend'
import { NotebooksGettingStartedTab } from './NotebooksGettingStartedTab'
import { NotebooksList, type NotebooksListProps } from './NotebooksList'
@ -106,13 +106,13 @@ export const NotebooksListPage: React.FunctionComponent<React.PropsWithChildren<
[navigate, location, setSelectedTab, telemetryService, telemetryRecorder]
)
const orderOptions: FilteredConnectionFilter[] = [
const orderOptions: Filter[] = [
{
label: 'Order by',
type: 'select',
id: 'order',
tooltip: 'Order notebooks',
values: [
options: [
{
value: 'updated-at-desc',
label: 'Last update (descending)',

View File

@ -2,19 +2,19 @@ import React, { useCallback, useEffect, useMemo } from 'react'
import { mdiChevronRight } from '@mdi/js'
import classNames from 'classnames'
import { of, type Observable, forkJoin } from 'rxjs'
import { forkJoin, of, type Observable } from 'rxjs'
import { catchError, map, mergeMap } from 'rxjs/operators'
import { asError, type ErrorLike, isErrorLike, pluralize } from '@sourcegraph/common'
import { aggregateStreamingSearch, type ContentMatch, LATEST_VERSION } from '@sourcegraph/shared/src/search/stream'
import { asError, isErrorLike, pluralize, type ErrorLike } from '@sourcegraph/common'
import { LATEST_VERSION, aggregateStreamingSearch, type ContentMatch } from '@sourcegraph/shared/src/search/stream'
import { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { Link, PageHeader, Container, Code, H3, Text, Icon, Tooltip, ButtonLink, Alert } from '@sourcegraph/wildcard'
import { Alert, ButtonLink, Code, Container, H3, Icon, Link, PageHeader, Text, Tooltip } from '@sourcegraph/wildcard'
import { FilteredConnection, type FilteredConnectionFilter } from '../components/FilteredConnection'
import { FilteredConnection, type Filter } from '../components/FilteredConnection'
import { PageTitle } from '../components/PageTitle'
import { useFeatureFlag } from '../featureFlags/useFeatureFlag'
import { type FeatureFlagFields, SearchPatternType } from '../graphql-operations'
import { SearchPatternType, type FeatureFlagFields } from '../graphql-operations'
import { fetchFeatureFlags as defaultFetchFeatureFlags } from './backend'
@ -104,12 +104,12 @@ export function getFeatureFlagReferences(flagName: string, productGitVersion: st
)
}
const filters: FilteredConnectionFilter[] = [
const filters: Filter[] = [
{
id: 'filters',
label: 'Type',
type: 'select',
values: [
options: [
{
label: 'All',
value: 'all',

View File

@ -1,31 +1,31 @@
import React, { useCallback, useEffect, useMemo } from 'react'
import { mdiAlertCircle, mdiAlert, mdiArrowLeftBold, mdiArrowRightBold } from '@mdi/js'
import { mdiAlert, mdiAlertCircle, mdiArrowLeftBold, mdiArrowRightBold } from '@mdi/js'
import classNames from 'classnames'
import { type Observable, of, timer } from 'rxjs'
import { of, timer, type Observable } from 'rxjs'
import { catchError, concatMap, map, repeat, takeWhile } from 'rxjs/operators'
import { parse as _parseVersion, type SemVer } from 'semver'
import { Timestamp } from '@sourcegraph/branded/src/components/Timestamp'
import { asError, type ErrorLike, isErrorLike } from '@sourcegraph/common'
import { asError, isErrorLike, type ErrorLike } from '@sourcegraph/common'
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import {
LoadingSpinner,
useObservable,
Alert,
Container,
Icon,
Code,
Container,
ErrorAlert,
H3,
Icon,
LoadingSpinner,
PageHeader,
Text,
Tooltip,
PageHeader,
ErrorAlert,
useObservable,
} from '@sourcegraph/wildcard'
import { Collapsible } from '../components/Collapsible'
import { FilteredConnection, type FilteredConnectionFilter, type Connection } from '../components/FilteredConnection'
import { FilteredConnection, type Connection, type Filter } from '../components/FilteredConnection'
import { PageTitle } from '../components/PageTitle'
import type { OutOfBandMigrationFields } from '../graphql-operations'
@ -42,12 +42,12 @@ export interface SiteAdminMigrationsPageProps extends TelemetryProps, TelemetryV
now?: () => Date
}
const filters: FilteredConnectionFilter[] = [
const filters: Filter[] = [
{
id: 'filters',
label: 'Migration state',
type: 'select',
values: [
options: [
{
label: 'All',
value: 'all',

View File

@ -1,4 +1,4 @@
import React, { type ReactNode, useCallback, useEffect, useState } from 'react'
import React, { useCallback, useEffect, useState, type ReactNode } from 'react'
import { mdiChevronDown } from '@mdi/js'
import { VisuallyHidden } from '@reach/visually-hidden'
@ -30,7 +30,7 @@ import {
import {
FilteredConnection,
type FilteredConnectionFilter,
type Filter,
type FilteredConnectionQueryArguments,
} from '../components/FilteredConnection'
import { PageTitle } from '../components/PageTitle'
@ -45,12 +45,12 @@ export interface SiteAdminOutboundRequestsPageProps extends TelemetryProps, Tele
export type OutboundRequest = OutboundRequestsResult['outboundRequests']['nodes'][0]
const filters: FilteredConnectionFilter[] = [
const filters: Filter[] = [
{
id: 'filters',
label: 'Filter',
type: 'select',
values: [
options: [
{
label: 'All',
value: 'all',

View File

@ -6,46 +6,47 @@ import { useLocation, useNavigate } from 'react-router-dom'
import { dataOrThrowErrors, useQuery } from '@sourcegraph/http-client'
import { RepoLink } from '@sourcegraph/shared/src/components/RepoLink'
import { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import {
Alert,
Button,
Code,
Container,
ErrorAlert,
Icon,
Input,
Link,
LoadingSpinner,
PageHeader,
Input,
useDebounce,
Button,
Alert,
Text,
Code,
Icon,
Menu,
MenuButton,
MenuList,
MenuItem,
MenuLink,
MenuList,
PageHeader,
Position,
Text,
useDebounce,
} from '@sourcegraph/wildcard'
import { externalRepoIcon } from '../components/externalServices/externalServices'
import {
buildFilterArgs,
FilterControl,
type FilteredConnectionFilter,
type FilteredConnectionFilterValue,
type Filter,
type FilterOption,
type FilterValues,
} from '../components/FilteredConnection'
import { useShowMorePagination } from '../components/FilteredConnection/hooks/useShowMorePagination'
import { ConnectionSummary } from '../components/FilteredConnection/ui'
import { getFilterFromURL, getUrlQuery } from '../components/FilteredConnection/utils'
import { PageTitle } from '../components/PageTitle'
import type {
ExternalServiceKindsResult,
ExternalServiceKindsVariables,
PackagesResult,
PackagesVariables,
SiteAdminPackageFields,
ExternalServiceKindsVariables,
ExternalServiceKindsResult,
} from '../graphql-operations'
import { EXTERNAL_SERVICE_KINDS, PACKAGES_QUERY } from './backend'
@ -179,7 +180,7 @@ export const SiteAdminPackagesPage: React.FunctionComponent<React.PropsWithChild
error: extSvcError,
} = useQuery<ExternalServiceKindsResult, ExternalServiceKindsVariables>(EXTERNAL_SERVICE_KINDS, {})
const ecosystemFilterValues = useMemo<FilteredConnectionFilterValue[]>(() => {
const ecosystemFilterValues = useMemo<FilterOption[]>(() => {
const values = []
for (const extSvc of extSvcs?.externalServices.nodes ?? []) {
@ -196,13 +197,13 @@ export const SiteAdminPackagesPage: React.FunctionComponent<React.PropsWithChild
return values
}, [extSvcs?.externalServices.nodes])
const filters = useMemo<FilteredConnectionFilter[]>(
const filters = useMemo<Filter[]>(
() => [
{
id: 'ecosystem',
label: 'Ecosystem',
type: 'select',
values: [
options: [
{
label: 'All',
value: 'all',
@ -215,7 +216,7 @@ export const SiteAdminPackagesPage: React.FunctionComponent<React.PropsWithChild
[ecosystemFilterValues]
)
const [filterValues, setFilterValues] = useState<Map<string, FilteredConnectionFilterValue>>(() =>
const [filterValues, setFilterValues] = useState<FilterValues>(() =>
getFilterFromURL(new URLSearchParams(location.search), filters)
)
@ -254,7 +255,7 @@ export const SiteAdminPackagesPage: React.FunctionComponent<React.PropsWithChild
}, [filterValues, filters, searchValue, location, navigate])
const variables = useMemo<PackagesVariables>(() => {
const args = buildFilterArgs(filterValues)
const args = buildFilterArgs(filters, filterValues)
return {
name: query,
@ -263,7 +264,7 @@ export const SiteAdminPackagesPage: React.FunctionComponent<React.PropsWithChild
first: DEFAULT_FIRST,
...args,
}
}, [filterValues, query])
}, [filters, filterValues, query])
const {
connection,
@ -343,12 +344,8 @@ export const SiteAdminPackagesPage: React.FunctionComponent<React.PropsWithChild
<FilterControl
filters={filters}
values={filterValues}
onValueSelect={(filter: FilteredConnectionFilter, value: FilteredConnectionFilterValue) =>
setFilterValues(values => {
const newValues = new Map(values)
newValues.set(filter.id, value)
return newValues
})
onValueSelect={(filter: Filter, value: FilterOption['value'] | null) =>
setFilterValues(values => ({ ...values, [filter.id]: value }))
}
/>
{connection && (

View File

@ -10,8 +10,9 @@ import { EXTERNAL_SERVICE_IDS_AND_NAMES } from '../components/externalServices/b
import {
buildFilterArgs,
FilterControl,
type FilteredConnectionFilter,
type FilteredConnectionFilterValue,
type Filter,
type FilterOption,
type FilterValues,
} from '../components/FilteredConnection'
import { usePageSwitcherPagination } from '../components/FilteredConnection/hooks/usePageSwitcherPagination'
import { getFilterFromURL, getUrlQuery } from '../components/FilteredConnection/utils'
@ -32,7 +33,7 @@ import { RepositoryNode } from './RepositoryNode'
import styles from './SiteAdminRepositoriesContainer.module.scss'
const STATUS_FILTERS: { [label: string]: FilteredConnectionFilterValue } = {
const STATUS_FILTERS: { [label: string]: FilterOption } = {
All: {
label: 'All',
value: 'all',
@ -83,12 +84,12 @@ const STATUS_FILTERS: { [label: string]: FilteredConnectionFilterValue } = {
},
}
const FILTERS: FilteredConnectionFilter[] = [
const FILTERS: Filter[] = [
{
id: 'order',
label: 'Order',
type: 'select',
values: [
options: [
{
label: 'Name (A-Z)',
value: 'name-asc',
@ -131,7 +132,7 @@ const FILTERS: FilteredConnectionFilter[] = [
id: 'status',
label: 'Status',
type: 'select',
values: Object.values(STATUS_FILTERS),
options: Object.values(STATUS_FILTERS),
},
]
@ -193,13 +194,13 @@ export const SiteAdminRepositoriesContainer: React.FunctionComponent<{ alwaysPol
id: 'codeHost',
label: 'Code Host',
type: 'select',
values,
options: values,
})
}
return filtersWithExternalServices
}, [extSvcs, location.pathname])
const [filterValues, setFilterValues] = useState<Map<string, FilteredConnectionFilterValue>>(() =>
const [filterValues, setFilterValues] = useState<FilterValues>(() =>
getFilterFromURL(new URLSearchParams(location.search), filters)
)
@ -240,7 +241,7 @@ export const SiteAdminRepositoriesContainer: React.FunctionComponent<{ alwaysPol
}, [filters, filterValues, searchQuery, location, navigate])
const variables = useMemo<RepositoriesVariables>(() => {
const args = buildFilterArgs(filterValues)
const args = buildFilterArgs(filters, filterValues)
return {
...args,
@ -252,7 +253,7 @@ export const SiteAdminRepositoriesContainer: React.FunctionComponent<{ alwaysPol
cloneStatus: args.cloneStatus ?? null,
externalService: args.externalService ?? null,
} as RepositoriesVariables
}, [searchQuery, filterValues])
}, [filters, searchQuery, filterValues])
const debouncedVariables = useDebounce(variables, 300)
@ -295,12 +296,7 @@ export const SiteAdminRepositoriesContainer: React.FunctionComponent<{ alwaysPol
color: 'var(--body-color)',
position: 'right',
tooltip: 'The number of repositories that are queued to be cloned.',
onClick: () =>
setFilterValues(values => {
const newValues = new Map(values)
newValues.set('status', STATUS_FILTERS.NotCloned)
return newValues
}),
onClick: () => setFilterValues(values => ({ ...values, status: STATUS_FILTERS.NotCloned.value })),
},
{
value: data.repositoryStats.cloning,
@ -308,12 +304,7 @@ export const SiteAdminRepositoriesContainer: React.FunctionComponent<{ alwaysPol
color: data.repositoryStats.cloning > 0 ? 'var(--primary)' : 'var(--body-color)',
position: 'right',
tooltip: 'The number of repositories that are currently being cloned.',
onClick: () =>
setFilterValues(values => {
const newValues = new Map(values)
newValues.set('status', STATUS_FILTERS.Cloning)
return newValues
}),
onClick: () => setFilterValues(values => ({ ...values, status: STATUS_FILTERS.Cloning.value })),
},
{
value: data.repositoryStats.cloned,
@ -321,12 +312,7 @@ export const SiteAdminRepositoriesContainer: React.FunctionComponent<{ alwaysPol
color: 'var(--success)',
position: 'right',
tooltip: 'The number of repositories that have been cloned.',
onClick: () =>
setFilterValues(values => {
const newValues = new Map(values)
newValues.set('status', STATUS_FILTERS.Cloned)
return newValues
}),
onClick: () => setFilterValues(values => ({ ...values, status: STATUS_FILTERS.Cloned.value })),
},
{
value: data.repositoryStats.indexed,
@ -334,12 +320,7 @@ export const SiteAdminRepositoriesContainer: React.FunctionComponent<{ alwaysPol
color: 'var(--body-color)',
position: 'right',
tooltip: 'The number of repositories that have been indexed for search.',
onClick: () =>
setFilterValues(values => {
const newValues = new Map(values)
newValues.set('status', STATUS_FILTERS.Indexed)
return newValues
}),
onClick: () => setFilterValues(values => ({ ...values, status: STATUS_FILTERS.Indexed.value })),
},
{
value: data.repositoryStats.failedFetch,
@ -348,11 +329,7 @@ export const SiteAdminRepositoriesContainer: React.FunctionComponent<{ alwaysPol
position: 'right',
tooltip: 'The number of repositories where the last syncing attempt produced an error.',
onClick: () =>
setFilterValues(values => {
const newValues = new Map(values)
newValues.set('status', STATUS_FILTERS.FailedFetchOrClone)
return newValues
}),
setFilterValues(values => ({ ...values, status: STATUS_FILTERS.FailedFetchOrClone.value })),
},
]
@ -364,12 +341,7 @@ export const SiteAdminRepositoriesContainer: React.FunctionComponent<{ alwaysPol
position: 'right',
tooltip:
'The number of repositories where corruption has been detected. Reclone these repositories to get rid of corruption.',
onClick: () =>
setFilterValues(values => {
const newValues = new Map(values)
newValues.set('status', STATUS_FILTERS.Corrupted)
return newValues
}),
onClick: () => setFilterValues(values => ({ ...values, status: STATUS_FILTERS.Corrupted.value })),
})
}
return items
@ -387,12 +359,8 @@ export const SiteAdminRepositoriesContainer: React.FunctionComponent<{ alwaysPol
<FilterControl
filters={filters}
values={filterValues}
onValueSelect={(filter: FilteredConnectionFilter, value: FilteredConnectionFilterValue) =>
setFilterValues(values => {
const newValues = new Map(values)
newValues.set(filter.id, value)
return newValues
})
onValueSelect={(filter: Filter, value: FilterOption['value'] | null) =>
setFilterValues(values => ({ ...values, [filter.id]: value }))
}
/>
<Input

View File

@ -1,4 +1,4 @@
import React, { type ReactNode, useCallback, useEffect, useState } from 'react'
import React, { useCallback, useEffect, useState, type ReactNode } from 'react'
import { mdiChevronDown, mdiContentCopy } from '@mdi/js'
import classNames from 'classnames'
@ -27,7 +27,7 @@ import {
import { requestGraphQL } from '../backend/graphql'
import {
FilteredConnection,
type FilteredConnectionFilter,
type Filter,
type FilteredConnectionQueryArguments,
} from '../components/FilteredConnection'
import { PageTitle } from '../components/PageTitle'
@ -41,12 +41,12 @@ export interface SiteAdminSlowRequestsPageProps extends TelemetryProps, Telemetr
type SlowRequest = SlowRequestsResult['slowRequests']['nodes'][0]
const filters: FilteredConnectionFilter[] = [
const filters: Filter[] = [
{
id: 'filters',
label: 'Filter',
type: 'select',
values: [
options: [
{
label: 'All',
value: 'all',

View File

@ -8,28 +8,28 @@ import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import { EVENT_LOGGER } from '@sourcegraph/shared/src/telemetry/web/eventLogger'
import {
Badge,
type BADGE_VARIANTS,
Button,
useLocalStorage,
Card,
H2,
H3,
Link,
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
H2,
H3,
Text,
Card,
useLocalStorage,
type BADGE_VARIANTS,
} from '@sourcegraph/wildcard'
import { FilteredConnection, type FilteredConnectionFilter } from '../components/FilteredConnection'
import { FilteredConnection, type Filter } from '../components/FilteredConnection'
import { PageTitle } from '../components/PageTitle'
import {
UserActivePeriod,
type SurveyResponseAggregateFields,
type SurveyResponseFields,
type UserWithSurveyResponseFields,
UserActivePeriod,
} from '../graphql-operations'
import {
fetchAllSurveyResponses,
@ -43,12 +43,12 @@ import { ValueLegendItem } from './analytics/components/ValueLegendList'
import styles from './SiteAdminSurveyResponsesPage.module.scss'
const USER_ACTIVITY_FILTERS: FilteredConnectionFilter[] = [
const USER_ACTIVITY_FILTERS: Filter[] = [
{
label: '',
type: 'radio',
id: 'user-activity-filters',
values: [
options: [
{
label: 'All users',
value: 'all',

View File

@ -1,6 +1,6 @@
import { Modal, PageHeader } from '@sourcegraph/wildcard'
import type { FilteredConnectionFilterValue } from '../../components/FilteredConnection'
import type { FilterOption } from '../../components/FilteredConnection'
import {
AddPackageFilterModalContent,
@ -11,7 +11,7 @@ import styles from './PackagesModal.module.scss'
interface AddFilterModalProps extends AddPackageFilterModalContentProps {
onDismiss: () => void
filters: FilteredConnectionFilterValue[]
filters: FilterOption[]
}
export const AddFilterModal: React.FunctionComponent<AddFilterModalProps> = props => (

View File

@ -2,7 +2,7 @@ import { useState } from 'react'
import { Button, Modal, PageHeader } from '@sourcegraph/wildcard'
import type { FilteredConnectionFilterValue } from '../../components/FilteredConnection'
import type { FilterOption } from '../../components/FilteredConnection'
import type { PackageRepoFilterFields } from '../../graphql-operations'
import { EditPackageFilterModalContent } from './modal-content/EditPackageFilterModalContent'
@ -16,7 +16,7 @@ import styles from './PackagesModal.module.scss'
interface ManageFiltersModalProps extends Omit<ManagePackageFiltersModalContentProps, 'setActiveFilter'> {
onDismiss: () => void
onAdd: () => void
filters: FilteredConnectionFilterValue[]
filters: FilterOption[]
}
export const ManageFiltersModal: React.FunctionComponent<ManageFiltersModalProps> = props => {

View File

@ -5,20 +5,20 @@ import classNames from 'classnames'
import { RepoLink } from '@sourcegraph/shared/src/components/RepoLink'
import {
Alert,
Button,
ErrorAlert,
Form,
Icon,
Input,
Label,
Alert,
LoadingSpinner,
Select,
Tooltip,
useDebounce,
LoadingSpinner,
ErrorAlert,
Select,
Form,
} from '@sourcegraph/wildcard'
import type { FilteredConnectionFilterValue } from '../../../components/FilteredConnection'
import type { FilterOption } from '../../../components/FilteredConnection'
import type { PackageRepoReferenceKind } from '../../../graphql-operations'
import { prettyBytesBigint } from '../../../util/prettyBytesBigint'
import { useMatchingPackages } from '../hooks/useMatchingPackages'
@ -35,7 +35,7 @@ export interface MultiPackageState {
interface MultiPackageFormProps {
initialState: MultiPackageState
filters: FilteredConnectionFilterValue[]
filters: FilterOption[]
setType: (type: BlockType) => void
onDismiss: () => void
onSave: (state: MultiPackageState) => Promise<unknown>

View File

@ -1,26 +1,26 @@
import { useState, useCallback } from 'react'
import { useCallback, useState } from 'react'
import { mdiPlus } from '@mdi/js'
import classNames from 'classnames'
import { toRepoURL } from '@sourcegraph/shared/src/util/url'
import {
Badge,
Button,
ErrorAlert,
Form,
Icon,
Input,
Label,
Tooltip,
Select,
LoadingSpinner,
ErrorAlert,
Badge,
useDebounce,
Form,
Link,
LoadingSpinner,
Select,
Tooltip,
useDebounce,
} from '@sourcegraph/wildcard'
import type { FilteredConnectionFilterValue } from '../../../components/FilteredConnection'
import type { PackageRepoReferenceKind, PackageRepoMatchFields } from '../../../graphql-operations'
import type { FilterOption } from '../../../components/FilteredConnection'
import type { PackageRepoMatchFields, PackageRepoReferenceKind } from '../../../graphql-operations'
import { useMatchingPackages } from '../hooks/useMatchingPackages'
import { useMatchingVersions } from '../hooks/useMatchingVersions'
import type { BlockType } from '../modal-content/AddPackageFilterModalContent'
@ -37,7 +37,7 @@ export interface SinglePackageState {
interface SinglePackageFormProps {
initialState: SinglePackageState
filters: FilteredConnectionFilterValue[]
filters: FilterOption[]
setType: (type: BlockType) => void
onDismiss: () => void
onSave: (state: SinglePackageState) => Promise<unknown>

View File

@ -3,11 +3,11 @@ import { useState } from 'react'
import { useMutation } from '@sourcegraph/http-client'
import { ErrorAlert } from '@sourcegraph/wildcard'
import type { FilteredConnectionFilterValue } from '../../../components/FilteredConnection'
import type { FilterOption } from '../../../components/FilteredConnection'
import {
PackageMatchBehaviour,
type AddPackageRepoFilterResult,
type AddPackageRepoFilterVariables,
PackageMatchBehaviour,
type PackageRepoReferenceKind,
type SiteAdminPackageFields,
} from '../../../graphql-operations'
@ -20,7 +20,7 @@ import styles from './AddPackageFilterModalContent.module.scss'
export interface AddPackageFilterModalContentProps {
node?: SiteAdminPackageFields
filters: FilteredConnectionFilterValue[]
filters: FilterOption[]
onDismiss: () => void
}

View File

@ -3,12 +3,12 @@ import { useState } from 'react'
import { useMutation } from '@sourcegraph/http-client'
import { ErrorAlert } from '@sourcegraph/wildcard'
import type { FilteredConnectionFilterValue } from '../../../components/FilteredConnection'
import type { FilterOption } from '../../../components/FilteredConnection'
import type {
UpdatePackageRepoFilterVariables,
PackageMatchBehaviour,
PackageRepoFilterFields,
UpdatePackageRepoFilterResult,
UpdatePackageRepoFilterVariables,
} from '../../../graphql-operations'
import { updatePackageRepoFilterMutation } from '../backend'
import { BehaviourSelect } from '../components/BehaviourSelect'
@ -42,7 +42,7 @@ const getInitialState = (packageFilter: PackageRepoFilterFields): SinglePackageS
export interface EditPackageFilterModalContentProps {
packageFilter: PackageRepoFilterFields
filters: FilteredConnectionFilterValue[]
filters: FilterOption[]
onDismiss: () => void
}