/********************************************************************************
* Copyright (c) 2020 TypeFox and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
import { FunctionComponent, SyntheticEvent, createContext, useContext, useEffect, useMemo, useState } from 'react';
import {
Alert,
Autocomplete,
Avatar,
Box,
Chip,
IconButton,
InputBase,
MenuItem,
Paper,
Select,
Stack,
Typography
} from '@mui/material';
import type { AutocompleteInputChangeReason, AutocompleteRenderInputParams } from '@mui/material';
import { useQueryClient } from '@tanstack/react-query';
import SearchIcon from '@mui/icons-material/Search';
import ClearIcon from '@mui/icons-material/Clear';
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
import PersonIcon from '@mui/icons-material/Person';
import { useParams, useNavigate } from 'react-router';
import { UserRelationships } from '../../extension-registry-types';
import { ErrorResponse } from '../../server-request';
import { MainContext } from '../../context';
import { PublisherDetails } from './publisher-details';
import { SearchListContainer } from './search-list-container';
import { handleError as formatError } from '../../utils';
import { AdminDashboardRoutes } from './admin-dashboard-routes';
import { useDebouncedCallback } from '../../hooks/use-debounced-callback';
import { useInfinitePublishers } from './use-publisher-admin';
// eslint-disable-next-line react-refresh/only-export-components
export const UpdateContext = createContext({ handleUpdate: () => {} });
const ROLE_FILTER_OPTIONS = [
{ value: '', label: 'Any role' },
{ value: 'admin', label: 'Admin' },
{ value: 'privileged', label: 'Privileged' },
{ value: 'none', label: 'No role' }
];
// How close to the bottom of the dropdown (in px) the user must scroll before the next page loads.
const LOAD_MORE_THRESHOLD = 200;
const roleIcon = (role: string | undefined) =>
role ? : ;
export const PublisherAdmin: FunctionComponent = () => {
const { publisher: publisherParam } = useParams<{ publisher?: string }>();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { pageSettings } = useContext(MainContext);
const [searchText, setSearchText] = useState(publisherParam ?? '');
const [inputValue, setInputValue] = useState(publisherParam ?? '');
const [roleFilter, setRoleFilter] = useState('');
const [selected, setSelected] = useState(null);
const debouncedSetSearch = useDebouncedCallback(setSearchText);
const { data, isFetching, isFetchingNextPage, error, hasNextPage, fetchNextPage } = useInfinitePublishers(
searchText,
roleFilter
);
const publishers = useMemo(() => data?.pages.flatMap(page => page.content) ?? [], [data]);
const listLoading = isFetching && !isFetchingNextPage;
// Resolve a deep-linked publisher once the matching page has loaded.
useEffect(() => {
if (publisherParam && !selected) {
const match = publishers.find(p => p.user.loginName === publisherParam);
if (match) {
setSelected(match);
}
}
}, [publisherParam, selected, publishers]);
const updateContextValue = useMemo(
() => ({
handleUpdate: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'publisher'] });
queryClient.invalidateQueries({ queryKey: ['admin', 'publishers'] });
}
}),
[queryClient]
);
const clearSelection = () => {
if (selected || publisherParam) {
setSelected(null);
navigate(AdminDashboardRoutes.PUBLISHER_ADMIN, { replace: true });
}
};
const handleSelect = (_event: SyntheticEvent, value: UserRelationships | null) => {
if (!value) {
clearSelection();
return;
}
setSelected(value);
setInputValue(value.user.loginName);
navigate(`${AdminDashboardRoutes.PUBLISHER_ADMIN}/${encodeURIComponent(value.user.loginName)}`, {
replace: true
});
};
const handleInputChange = (_event: SyntheticEvent, value: string, reason: AutocompleteInputChangeReason) => {
setInputValue(value);
// 'reset' fires when the input syncs to the selected option's label — nothing else to do.
if (reason === 'reset') {
return;
}
clearSelection();
if (reason === 'clear') {
setSearchText('');
} else {
debouncedSetSearch(value);
}
};
const handleClear = () => {
setInputValue('');
setSearchText('');
clearSelection();
};
const loadMoreOnScroll = (event: SyntheticEvent) => {
const listbox = event.currentTarget as HTMLElement;
const reachedBottom = listbox.scrollHeight - listbox.scrollTop - listbox.clientHeight < LOAD_MORE_THRESHOLD;
if (reachedBottom && hasNextPage && !isFetchingNextPage) {
void fetchNextPage();
}
};
const searchIconColor = pageSettings?.themeType === 'dark' ? '#111111' : '#ffffff';
const renderInput = (params: AutocompleteRenderInputParams) => (
{inputValue && (
)}
}
/>
);
return (
option.user.loginName}
isOptionEqualToValue={(option, value) =>
option.user.loginName === value.user.loginName &&
option.user.provider === value.user.provider
}
filterOptions={x => x}
autoHighlight
clearOnBlur={false}
handleHomeEndKeys
forcePopupIcon={false}
loading={listLoading}
loadingText='Searching…'
noOptionsText='No publishers matched the current filters.'
ListboxProps={{ onScroll: loadMoreOnScroll }}
renderInput={renderInput}
renderOption={(props, option) => {
const { user } = option;
return (
{user.loginName}
{user.fullName || '—'}
{user.role ? (
) : null}
);
}}
/>
]}
listContainer={null}
loading={listLoading}
/>
{error && {formatError(error as Error | Partial)}}
{selected ? (
) : publisherParam && !isFetching ? (
No publisher found for “{publisherParam}”.
) : (
Search for a user and select one to view their details.
)}
);
};