mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +02:00
Feat/pagination loading (#5325)
This PR makes changes to how the project overview skeleton screen works. Important changes: - Add skeleton screens to missing elements, creating a more comprehensive loading screen - Split the page into different loading sections, so that we can load the table when we fetch the next page without affecting the rest of the page. https://www.loom.com/share/e5d30dc897ac488ea80cfae11ffab646 Next steps: * Hide bar if total is less than 25 * Add FE testing
This commit is contained in:
parent
6a41ee6e9d
commit
834ae1d8a4
@ -81,6 +81,7 @@ export const Search = ({
|
|||||||
containerStyles,
|
containerStyles,
|
||||||
expandable = false,
|
expandable = false,
|
||||||
debounceTime = 200,
|
debounceTime = 200,
|
||||||
|
...rest
|
||||||
}: ISearchProps) => {
|
}: ISearchProps) => {
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
const searchContainerRef = useRef<HTMLInputElement>(null);
|
const searchContainerRef = useRef<HTMLInputElement>(null);
|
||||||
@ -126,6 +127,7 @@ export const Search = ({
|
|||||||
ref={searchContainerRef}
|
ref={searchContainerRef}
|
||||||
style={containerStyles}
|
style={containerStyles}
|
||||||
active={expandable && showSuggestions}
|
active={expandable && showSuggestions}
|
||||||
|
{...rest}
|
||||||
>
|
>
|
||||||
<StyledSearch className={className}>
|
<StyledSearch className={className}>
|
||||||
<SearchIcon
|
<SearchIcon
|
||||||
|
@ -13,7 +13,7 @@ export const SortableTableHeader = <T extends object>({
|
|||||||
}) => (
|
}) => (
|
||||||
<TableHead className={className}>
|
<TableHead className={className}>
|
||||||
{headerGroups.map((headerGroup) => (
|
{headerGroups.map((headerGroup) => (
|
||||||
<TableRow {...headerGroup.getHeaderGroupProps()}>
|
<TableRow {...headerGroup.getHeaderGroupProps()} data-loading>
|
||||||
{headerGroup.headers.map((column: HeaderGroup<T>) => {
|
{headerGroup.headers.map((column: HeaderGroup<T>) => {
|
||||||
const content = column.render('Header');
|
const content = column.render('Header');
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ interface IFeatureSeenCellProps {
|
|||||||
|
|
||||||
export const FeatureEnvironmentSeenCell: VFC<IFeatureSeenCellProps> = ({
|
export const FeatureEnvironmentSeenCell: VFC<IFeatureSeenCellProps> = ({
|
||||||
feature,
|
feature,
|
||||||
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const environments = feature.environments
|
const environments = feature.environments
|
||||||
? Object.values(feature.environments)
|
? Object.values(feature.environments)
|
||||||
@ -17,6 +18,7 @@ export const FeatureEnvironmentSeenCell: VFC<IFeatureSeenCellProps> = ({
|
|||||||
<FeatureEnvironmentSeen
|
<FeatureEnvironmentSeen
|
||||||
featureLastSeen={feature.lastSeenAt}
|
featureLastSeen={feature.lastSeenAt}
|
||||||
environments={environments}
|
environments={environments}
|
||||||
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -72,7 +72,7 @@ export const LastSeenTooltip = ({
|
|||||||
Boolean(environment.lastSeenAt),
|
Boolean(environment.lastSeenAt),
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<StyledDescription {...rest}>
|
<StyledDescription {...rest} data-loading>
|
||||||
<StyledDescriptionHeader sx={{ mb: 0 }}>
|
<StyledDescriptionHeader sx={{ mb: 0 }}>
|
||||||
Last usage reported
|
Last usage reported
|
||||||
</StyledDescriptionHeader>
|
</StyledDescriptionHeader>
|
||||||
|
@ -74,6 +74,7 @@ export const FeatureEnvironmentSeen = ({
|
|||||||
featureLastSeen,
|
featureLastSeen,
|
||||||
environments,
|
environments,
|
||||||
sx,
|
sx,
|
||||||
|
...rest
|
||||||
}: IFeatureEnvironmentSeenProps) => {
|
}: IFeatureEnvironmentSeenProps) => {
|
||||||
const getColor = useLastSeenColors();
|
const getColor = useLastSeenColors();
|
||||||
|
|
||||||
@ -95,6 +96,7 @@ export const FeatureEnvironmentSeen = ({
|
|||||||
<LastSeenTooltip
|
<LastSeenTooltip
|
||||||
featureLastSeen={lastSeen}
|
featureLastSeen={lastSeen}
|
||||||
environments={environments}
|
environments={environments}
|
||||||
|
{...rest}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
color={color}
|
color={color}
|
||||||
|
@ -228,6 +228,7 @@ export const Project = () => {
|
|||||||
{filteredTabs.map((tab) => {
|
{filteredTabs.map((tab) => {
|
||||||
return (
|
return (
|
||||||
<StyledTab
|
<StyledTab
|
||||||
|
data-loading
|
||||||
key={tab.title}
|
key={tab.title}
|
||||||
label={tab.title}
|
label={tab.title}
|
||||||
value={tab.path}
|
value={tab.path}
|
||||||
|
@ -87,7 +87,7 @@ export const ActionsCell: VFC<IActionsCellProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledBoxCell>
|
<StyledBoxCell data-loading>
|
||||||
<Tooltip title='Feature toggle actions' arrow describeChild>
|
<Tooltip title='Feature toggle actions' arrow describeChild>
|
||||||
<IconButton
|
<IconButton
|
||||||
id={id}
|
id={id}
|
||||||
|
@ -62,6 +62,7 @@ import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
|||||||
import { ListItemType } from './ProjectFeatureToggles.types';
|
import { ListItemType } from './ProjectFeatureToggles.types';
|
||||||
import { createFeatureToggleCell } from './FeatureToggleSwitch/createFeatureToggleCell';
|
import { createFeatureToggleCell } from './FeatureToggleSwitch/createFeatureToggleCell';
|
||||||
import { useFeatureToggleSwitch } from './FeatureToggleSwitch/useFeatureToggleSwitch';
|
import { useFeatureToggleSwitch } from './FeatureToggleSwitch/useFeatureToggleSwitch';
|
||||||
|
import useLoading from 'hooks/useLoading';
|
||||||
|
|
||||||
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
@ -73,6 +74,7 @@ interface IPaginatedProjectFeatureTogglesProps {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
onChange: () => void;
|
onChange: () => void;
|
||||||
total?: number;
|
total?: number;
|
||||||
|
initialLoad: boolean;
|
||||||
searchValue: string;
|
searchValue: string;
|
||||||
setSearchValue: React.Dispatch<React.SetStateAction<string>>;
|
setSearchValue: React.Dispatch<React.SetStateAction<string>>;
|
||||||
paginationBar: JSX.Element;
|
paginationBar: JSX.Element;
|
||||||
@ -87,6 +89,7 @@ const defaultSort: SortingRule<string> & {
|
|||||||
export const PaginatedProjectFeatureToggles = ({
|
export const PaginatedProjectFeatureToggles = ({
|
||||||
features,
|
features,
|
||||||
loading,
|
loading,
|
||||||
|
initialLoad,
|
||||||
environments: newEnvironments = [],
|
environments: newEnvironments = [],
|
||||||
onChange,
|
onChange,
|
||||||
total,
|
total,
|
||||||
@ -95,6 +98,8 @@ export const PaginatedProjectFeatureToggles = ({
|
|||||||
paginationBar,
|
paginationBar,
|
||||||
}: IPaginatedProjectFeatureTogglesProps) => {
|
}: IPaginatedProjectFeatureTogglesProps) => {
|
||||||
const { classes: styles } = useStyles();
|
const { classes: styles } = useStyles();
|
||||||
|
const bodyLoadingRef = useLoading(loading);
|
||||||
|
const headerLoadingRef = useLoading(initialLoad);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const [strategiesDialogState, setStrategiesDialogState] = useState({
|
const [strategiesDialogState, setStrategiesDialogState] = useState({
|
||||||
@ -198,7 +203,10 @@ export const PaginatedProjectFeatureToggles = ({
|
|||||||
accessor: 'lastSeenAt',
|
accessor: 'lastSeenAt',
|
||||||
Cell: ({ value, row: { original: feature } }: any) => {
|
Cell: ({ value, row: { original: feature } }: any) => {
|
||||||
return showEnvironmentLastSeen ? (
|
return showEnvironmentLastSeen ? (
|
||||||
<MemoizedFeatureEnvironmentSeenCell feature={feature} />
|
<MemoizedFeatureEnvironmentSeenCell
|
||||||
|
feature={feature}
|
||||||
|
data-loading
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FeatureSeenCell value={value} />
|
<FeatureSeenCell value={value} />
|
||||||
);
|
);
|
||||||
@ -355,15 +363,20 @@ export const PaginatedProjectFeatureToggles = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const data = useMemo(() => {
|
const data = useMemo(() => {
|
||||||
if (loading) {
|
if (initialLoad || loading) {
|
||||||
return Array(6).fill({
|
const loadingData = Array(15)
|
||||||
type: '-',
|
.fill(null)
|
||||||
name: 'Feature name',
|
.map((_, index) => ({
|
||||||
createdAt: new Date(),
|
id: index, // Assuming `id` is a required property
|
||||||
environments: {
|
type: '-',
|
||||||
production: { name: 'production', enabled: false },
|
name: `Feature name ${index}`,
|
||||||
},
|
createdAt: new Date().toISOString(),
|
||||||
}) as FeatureSchema[];
|
environments: {
|
||||||
|
production: { name: 'production', enabled: false },
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
// Coerce loading data to FeatureSchema[]
|
||||||
|
return loadingData as unknown as FeatureSchema[];
|
||||||
}
|
}
|
||||||
return featuresData;
|
return featuresData;
|
||||||
}, [loading, featuresData]);
|
}, [loading, featuresData]);
|
||||||
@ -491,168 +504,193 @@ export const PaginatedProjectFeatureToggles = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageContent
|
<PageContent
|
||||||
isLoading={loading}
|
disableLoading
|
||||||
className={styles.container}
|
className={styles.container}
|
||||||
sx={{ borderBottomLeftRadius: 0, borderBottomRightRadius: 0 }}
|
sx={{ borderBottomLeftRadius: 0, borderBottomRightRadius: 0 }}
|
||||||
header={
|
header={
|
||||||
<PageHeader
|
<div ref={headerLoadingRef}>
|
||||||
titleElement={
|
<PageHeader
|
||||||
showTitle
|
titleElement={
|
||||||
? `Feature toggles (${total || rows.length})`
|
showTitle
|
||||||
: null
|
? `Feature toggles (${
|
||||||
}
|
total || rows.length
|
||||||
actions={
|
})`
|
||||||
<>
|
: null
|
||||||
<ConditionallyRender
|
|
||||||
condition={!isSmallScreen}
|
|
||||||
show={
|
|
||||||
<Search
|
|
||||||
placeholder='Search and Filter'
|
|
||||||
expandable
|
|
||||||
initialValue={searchValue}
|
|
||||||
onChange={setSearchValue}
|
|
||||||
onFocus={() => setShowTitle(false)}
|
|
||||||
onBlur={() => setShowTitle(true)}
|
|
||||||
hasFilters
|
|
||||||
getSearchContext={getSearchContext}
|
|
||||||
id='projectFeatureToggles'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ColumnsMenu
|
|
||||||
allColumns={allColumns}
|
|
||||||
staticColumns={staticColumns}
|
|
||||||
dividerAfter={['createdAt']}
|
|
||||||
dividerBefore={['Actions']}
|
|
||||||
isCustomized={Boolean(storedParams.columns)}
|
|
||||||
setHiddenColumns={setHiddenColumns}
|
|
||||||
/>
|
|
||||||
<PageHeader.Divider sx={{ marginLeft: 0 }} />
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(
|
|
||||||
uiConfig?.flags?.featuresExportImport,
|
|
||||||
)}
|
|
||||||
show={
|
|
||||||
<Tooltip
|
|
||||||
title='Export toggles visible in the table below'
|
|
||||||
arrow
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
onClick={() =>
|
|
||||||
setShowExportDialog(true)
|
|
||||||
}
|
|
||||||
sx={(theme) => ({
|
|
||||||
marginRight:
|
|
||||||
theme.spacing(2),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<FileDownload />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<StyledResponsiveButton
|
|
||||||
onClick={() =>
|
|
||||||
navigate(getCreateTogglePath(projectId))
|
|
||||||
}
|
|
||||||
maxWidth='960px'
|
|
||||||
Icon={Add}
|
|
||||||
projectId={projectId}
|
|
||||||
permission={CREATE_FEATURE}
|
|
||||||
data-testid='NAVIGATE_TO_CREATE_FEATURE'
|
|
||||||
>
|
|
||||||
New feature toggle
|
|
||||||
</StyledResponsiveButton>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={isSmallScreen}
|
|
||||||
show={
|
|
||||||
<Search
|
|
||||||
initialValue={searchValue}
|
|
||||||
onChange={setSearchValue}
|
|
||||||
hasFilters
|
|
||||||
getSearchContext={getSearchContext}
|
|
||||||
id='projectFeatureToggles'
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
/>
|
actions={
|
||||||
</PageHeader>
|
<>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!isSmallScreen}
|
||||||
|
show={
|
||||||
|
<Search
|
||||||
|
data-loading
|
||||||
|
placeholder='Search and Filter'
|
||||||
|
expandable
|
||||||
|
initialValue={searchValue}
|
||||||
|
onChange={setSearchValue}
|
||||||
|
onFocus={() =>
|
||||||
|
setShowTitle(false)
|
||||||
|
}
|
||||||
|
onBlur={() =>
|
||||||
|
setShowTitle(true)
|
||||||
|
}
|
||||||
|
hasFilters
|
||||||
|
getSearchContext={
|
||||||
|
getSearchContext
|
||||||
|
}
|
||||||
|
id='projectFeatureToggles'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ColumnsMenu
|
||||||
|
allColumns={allColumns}
|
||||||
|
staticColumns={staticColumns}
|
||||||
|
dividerAfter={['createdAt']}
|
||||||
|
dividerBefore={['Actions']}
|
||||||
|
isCustomized={Boolean(
|
||||||
|
storedParams.columns,
|
||||||
|
)}
|
||||||
|
setHiddenColumns={setHiddenColumns}
|
||||||
|
/>
|
||||||
|
<PageHeader.Divider
|
||||||
|
sx={{ marginLeft: 0 }}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(
|
||||||
|
uiConfig?.flags
|
||||||
|
?.featuresExportImport,
|
||||||
|
)}
|
||||||
|
show={
|
||||||
|
<Tooltip
|
||||||
|
title='Export toggles visible in the table below'
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
data-loading
|
||||||
|
onClick={() =>
|
||||||
|
setShowExportDialog(
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sx={(theme) => ({
|
||||||
|
marginRight:
|
||||||
|
theme.spacing(2),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<FileDownload />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StyledResponsiveButton
|
||||||
|
onClick={() =>
|
||||||
|
navigate(
|
||||||
|
getCreateTogglePath(projectId),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
maxWidth='960px'
|
||||||
|
Icon={Add}
|
||||||
|
projectId={projectId}
|
||||||
|
permission={CREATE_FEATURE}
|
||||||
|
data-testid='NAVIGATE_TO_CREATE_FEATURE'
|
||||||
|
>
|
||||||
|
New feature toggle
|
||||||
|
</StyledResponsiveButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={isSmallScreen}
|
||||||
|
show={
|
||||||
|
<Search
|
||||||
|
initialValue={searchValue}
|
||||||
|
onChange={setSearchValue}
|
||||||
|
hasFilters
|
||||||
|
getSearchContext={getSearchContext}
|
||||||
|
id='projectFeatureToggles'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PageHeader>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SearchHighlightProvider value={getSearchText(searchValue)}>
|
<div ref={bodyLoadingRef}>
|
||||||
<VirtualizedTable
|
<SearchHighlightProvider value={getSearchText(searchValue)}>
|
||||||
rows={rows}
|
<VirtualizedTable
|
||||||
headerGroups={headerGroups}
|
rows={rows}
|
||||||
prepareRow={prepareRow}
|
headerGroups={headerGroups}
|
||||||
|
prepareRow={prepareRow}
|
||||||
|
/>
|
||||||
|
</SearchHighlightProvider>
|
||||||
|
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={rows.length === 0}
|
||||||
|
show={
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={searchValue?.length > 0}
|
||||||
|
show={
|
||||||
|
<TablePlaceholder>
|
||||||
|
No feature toggles found matching
|
||||||
|
“
|
||||||
|
{searchValue}
|
||||||
|
”
|
||||||
|
</TablePlaceholder>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<TablePlaceholder>
|
||||||
|
No feature toggles available. Get
|
||||||
|
started by adding a new feature toggle.
|
||||||
|
</TablePlaceholder>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</SearchHighlightProvider>
|
<EnvironmentStrategyDialog
|
||||||
<ConditionallyRender
|
onClose={() =>
|
||||||
condition={rows.length === 0}
|
setStrategiesDialogState((prev) => ({
|
||||||
show={
|
...prev,
|
||||||
<ConditionallyRender
|
open: false,
|
||||||
condition={searchValue?.length > 0}
|
}))
|
||||||
show={
|
}
|
||||||
<TablePlaceholder>
|
projectId={projectId}
|
||||||
No feature toggles found matching “
|
{...strategiesDialogState}
|
||||||
{searchValue}
|
/>
|
||||||
”
|
<FeatureStaleDialog
|
||||||
</TablePlaceholder>
|
isStale={featureStaleDialogState.stale === true}
|
||||||
}
|
isOpen={Boolean(featureStaleDialogState.featureId)}
|
||||||
elseShow={
|
onClose={() => {
|
||||||
<TablePlaceholder>
|
setFeatureStaleDialogState({});
|
||||||
No feature toggles available. Get started by
|
onChange();
|
||||||
adding a new feature toggle.
|
}}
|
||||||
</TablePlaceholder>
|
featureId={featureStaleDialogState.featureId || ''}
|
||||||
}
|
projectId={projectId}
|
||||||
/>
|
/>
|
||||||
}
|
<FeatureArchiveDialog
|
||||||
/>
|
isOpen={Boolean(featureArchiveState)}
|
||||||
<EnvironmentStrategyDialog
|
onConfirm={onChange}
|
||||||
onClose={() =>
|
onClose={() => {
|
||||||
setStrategiesDialogState((prev) => ({
|
setFeatureArchiveState(undefined);
|
||||||
...prev,
|
}}
|
||||||
open: false,
|
featureIds={[featureArchiveState || '']}
|
||||||
}))
|
projectId={projectId}
|
||||||
}
|
/>
|
||||||
projectId={projectId}
|
<ConditionallyRender
|
||||||
{...strategiesDialogState}
|
condition={
|
||||||
/>
|
Boolean(uiConfig?.flags?.featuresExportImport) &&
|
||||||
<FeatureStaleDialog
|
!loading
|
||||||
isStale={featureStaleDialogState.stale === true}
|
}
|
||||||
isOpen={Boolean(featureStaleDialogState.featureId)}
|
show={
|
||||||
onClose={() => {
|
<ExportDialog
|
||||||
setFeatureStaleDialogState({});
|
showExportDialog={showExportDialog}
|
||||||
onChange();
|
data={data}
|
||||||
}}
|
onClose={() => setShowExportDialog(false)}
|
||||||
featureId={featureStaleDialogState.featureId || ''}
|
environments={environments}
|
||||||
projectId={projectId}
|
/>
|
||||||
/>
|
}
|
||||||
<FeatureArchiveDialog
|
/>
|
||||||
isOpen={Boolean(featureArchiveState)}
|
{featureToggleModals}
|
||||||
onConfirm={onChange}
|
</div>
|
||||||
onClose={() => {
|
|
||||||
setFeatureArchiveState(undefined);
|
|
||||||
}}
|
|
||||||
featureIds={[featureArchiveState || '']}
|
|
||||||
projectId={projectId}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={
|
|
||||||
Boolean(uiConfig?.flags?.featuresExportImport) &&
|
|
||||||
!loading
|
|
||||||
}
|
|
||||||
show={
|
|
||||||
<ExportDialog
|
|
||||||
showExportDialog={showExportDialog}
|
|
||||||
data={data}
|
|
||||||
onClose={() => setShowExportDialog(false)}
|
|
||||||
environments={environments}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{featureToggleModals}
|
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|
||||||
{paginationBar}
|
{paginationBar}
|
||||||
|
@ -20,7 +20,7 @@ export const RowSelectCell: FC<IRowSelectCellProps> = ({
|
|||||||
checked,
|
checked,
|
||||||
title,
|
title,
|
||||||
}) => (
|
}) => (
|
||||||
<StyledBoxCell data-testid={BATCH_SELECT}>
|
<StyledBoxCell data-testid={BATCH_SELECT} data-loading>
|
||||||
<Checkbox onChange={onChange} title={title} checked={checked} />
|
<Checkbox onChange={onChange} title={title} checked={checked} />
|
||||||
</StyledBoxCell>
|
</StyledBoxCell>
|
||||||
);
|
);
|
||||||
|
@ -40,7 +40,7 @@ export const HealthWidget = ({ projectId, health }: IHealthWidgetProps) => {
|
|||||||
gap: (theme) => theme.spacing(2),
|
gap: (theme) => theme.spacing(2),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StyledPercentageText>
|
<StyledPercentageText data-loading>
|
||||||
<PercentageCircle percentage={health} />
|
<PercentageCircle percentage={health} />
|
||||||
</StyledPercentageText>
|
</StyledPercentageText>
|
||||||
<StyledParagraphEmphasizedText data-loading>
|
<StyledParagraphEmphasizedText data-loading>
|
||||||
|
@ -24,8 +24,8 @@ const StyledIDContainer = styled('div')(({ theme }) => ({
|
|||||||
export const MetaWidget: FC<IMetaWidgetProps> = ({ id, description }) => {
|
export const MetaWidget: FC<IMetaWidgetProps> = ({ id, description }) => {
|
||||||
return (
|
return (
|
||||||
<StyledProjectInfoWidgetContainer>
|
<StyledProjectInfoWidgetContainer>
|
||||||
<StyledWidgetTitle>Project Meta</StyledWidgetTitle>
|
<StyledWidgetTitle data-loading>Project Meta</StyledWidgetTitle>
|
||||||
<StyledIDContainer>
|
<StyledIDContainer data-loading>
|
||||||
<Typography
|
<Typography
|
||||||
component='span'
|
component='span'
|
||||||
variant='body2'
|
variant='body2'
|
||||||
@ -39,6 +39,7 @@ export const MetaWidget: FC<IMetaWidgetProps> = ({ id, description }) => {
|
|||||||
condition={Boolean(description)}
|
condition={Boolean(description)}
|
||||||
show={
|
show={
|
||||||
<Typography
|
<Typography
|
||||||
|
data-loading
|
||||||
variant='body2'
|
variant='body2'
|
||||||
sx={{
|
sx={{
|
||||||
marginTop: (theme) => theme.spacing(1.5),
|
marginTop: (theme) => theme.spacing(1.5),
|
||||||
|
@ -30,6 +30,7 @@ export const ProjectMembersWidget = ({
|
|||||||
<StyledProjectInfoWidgetContainer>
|
<StyledProjectInfoWidgetContainer>
|
||||||
<StyledWidgetTitle data-loading>Project members</StyledWidgetTitle>
|
<StyledWidgetTitle data-loading>Project members</StyledWidgetTitle>
|
||||||
<Box
|
<Box
|
||||||
|
data-loading
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@ -37,7 +38,9 @@ export const ProjectMembersWidget = ({
|
|||||||
>
|
>
|
||||||
<StatusBox boxText={`${memberCount}`} change={change} />
|
<StatusBox boxText={`${memberCount}`} change={change} />
|
||||||
</Box>
|
</Box>
|
||||||
<WidgetFooterLink to={link}>View all members</WidgetFooterLink>
|
<WidgetFooterLink data-loading to={link}>
|
||||||
|
View all members
|
||||||
|
</WidgetFooterLink>
|
||||||
</StyledProjectInfoWidgetContainer>
|
</StyledProjectInfoWidgetContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -84,7 +84,9 @@ export const ToggleTypesWidget = ({ features }: IToggleTypesWidgetProps) => {
|
|||||||
<StyledProjectInfoWidgetContainer
|
<StyledProjectInfoWidgetContainer
|
||||||
sx={{ padding: (theme) => theme.spacing(3) }}
|
sx={{ padding: (theme) => theme.spacing(3) }}
|
||||||
>
|
>
|
||||||
<StyledWidgetTitle>Toggle types used</StyledWidgetTitle>
|
<StyledWidgetTitle data-loading>
|
||||||
|
Toggle types used
|
||||||
|
</StyledWidgetTitle>
|
||||||
{Object.keys(featureTypeStats).map((type) => (
|
{Object.keys(featureTypeStats).map((type) => (
|
||||||
<ToggleTypesRow
|
<ToggleTypesRow
|
||||||
type={type}
|
type={type}
|
||||||
|
@ -12,6 +12,7 @@ export const WidgetFooterLink: FC<IWidgetFooterLinkProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Typography
|
<Typography
|
||||||
|
data-loading
|
||||||
variant='body2'
|
variant='body2'
|
||||||
textAlign='center'
|
textAlign='center'
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -45,7 +45,7 @@ const PaginatedProjectOverview = () => {
|
|||||||
const { project, loading: projectLoading } = useProject(projectId, {
|
const { project, loading: projectLoading } = useProject(projectId, {
|
||||||
refreshInterval,
|
refreshInterval,
|
||||||
});
|
});
|
||||||
const [pageLimit, setPageLimit] = useState(10);
|
const [pageLimit, setPageLimit] = useState(25);
|
||||||
const [currentOffset, setCurrentOffset] = useState(0);
|
const [currentOffset, setCurrentOffset] = useState(0);
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = useState(
|
const [searchValue, setSearchValue] = useState(
|
||||||
@ -57,6 +57,7 @@ const PaginatedProjectOverview = () => {
|
|||||||
total,
|
total,
|
||||||
refetch,
|
refetch,
|
||||||
loading,
|
loading,
|
||||||
|
initialLoad,
|
||||||
} = useFeatureSearch(currentOffset, pageLimit, projectId, searchValue, {
|
} = useFeatureSearch(currentOffset, pageLimit, projectId, searchValue, {
|
||||||
refreshInterval,
|
refreshInterval,
|
||||||
});
|
});
|
||||||
@ -96,6 +97,7 @@ const PaginatedProjectOverview = () => {
|
|||||||
}
|
}
|
||||||
features={searchFeatures}
|
features={searchFeatures}
|
||||||
environments={environments}
|
environments={environments}
|
||||||
|
initialLoad={initialLoad && searchFeatures.length === 0}
|
||||||
loading={loading && searchFeatures.length === 0}
|
loading={loading && searchFeatures.length === 0}
|
||||||
onChange={refetch}
|
onChange={refetch}
|
||||||
total={total}
|
total={total}
|
||||||
@ -128,7 +130,7 @@ const StyledStickyBar = styled('div')(({ theme }) => ({
|
|||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
marginLeft: theme.spacing(2),
|
marginLeft: theme.spacing(2),
|
||||||
zIndex: theme.zIndex.mobileStepper,
|
zIndex: 9999,
|
||||||
borderBottomLeftRadius: theme.shape.borderRadiusMedium,
|
borderBottomLeftRadius: theme.shape.borderRadiusMedium,
|
||||||
borderBottomRightRadius: theme.shape.borderRadiusMedium,
|
borderBottomRightRadius: theme.shape.borderRadiusMedium,
|
||||||
borderTop: `1px solid ${theme.palette.divider}`,
|
borderTop: `1px solid ${theme.palette.divider}`,
|
||||||
|
@ -65,7 +65,11 @@ export const StatusBox: FC<IStatusBoxProps> = ({
|
|||||||
<>
|
<>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={Boolean(title)}
|
condition={Boolean(title)}
|
||||||
show={<StyledTypographyHeader>{title}</StyledTypographyHeader>}
|
show={
|
||||||
|
<StyledTypographyHeader data-loading>
|
||||||
|
{title}
|
||||||
|
</StyledTypographyHeader>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
<Box
|
<Box
|
||||||
@ -75,11 +79,13 @@ export const StatusBox: FC<IStatusBoxProps> = ({
|
|||||||
width: 'auto',
|
width: 'auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StyledTypographyCount>{boxText}</StyledTypographyCount>
|
<StyledTypographyCount data-loading>
|
||||||
|
{boxText}
|
||||||
|
</StyledTypographyCount>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={Boolean(customChangeElement)}
|
condition={Boolean(customChangeElement)}
|
||||||
show={
|
show={
|
||||||
<StyledBoxChangeContainer>
|
<StyledBoxChangeContainer data-loading>
|
||||||
{customChangeElement}
|
{customChangeElement}
|
||||||
</StyledBoxChangeContainer>
|
</StyledBoxChangeContainer>
|
||||||
}
|
}
|
||||||
@ -87,7 +93,7 @@ export const StatusBox: FC<IStatusBoxProps> = ({
|
|||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={change !== undefined && change !== 0}
|
condition={change !== undefined && change !== 0}
|
||||||
show={
|
show={
|
||||||
<StyledBoxChangeContainer>
|
<StyledBoxChangeContainer data-loading>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
...flexRow,
|
...flexRow,
|
||||||
@ -109,7 +115,7 @@ export const StatusBox: FC<IStatusBoxProps> = ({
|
|||||||
}
|
}
|
||||||
elseShow={
|
elseShow={
|
||||||
<StyledBoxChangeContainer>
|
<StyledBoxChangeContainer>
|
||||||
<StyledTypographySubtext>
|
<StyledTypographySubtext data-loading>
|
||||||
No change
|
No change
|
||||||
</StyledTypographySubtext>
|
</StyledTypographySubtext>
|
||||||
</StyledBoxChangeContainer>
|
</StyledBoxChangeContainer>
|
||||||
|
@ -119,6 +119,7 @@ exports[`renders an empty list correctly 1`] = `
|
|||||||
>
|
>
|
||||||
<tr
|
<tr
|
||||||
className="MuiTableRow-root MuiTableRow-head css-15lapfi-MuiTableRow-root"
|
className="MuiTableRow-root MuiTableRow-head css-15lapfi-MuiTableRow-root"
|
||||||
|
data-loading={true}
|
||||||
role="row"
|
role="row"
|
||||||
>
|
>
|
||||||
<th
|
<th
|
||||||
|
@ -14,6 +14,7 @@ interface IUseFeatureSearchOutput {
|
|||||||
features: IFeatureToggleListItem[];
|
features: IFeatureToggleListItem[];
|
||||||
total: number;
|
total: number;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
initialLoad: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
refetch: () => void;
|
refetch: () => void;
|
||||||
}
|
}
|
||||||
@ -26,38 +27,52 @@ const fallbackData: {
|
|||||||
total: 0,
|
total: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFeatureSearch = (
|
const createFeatureSearch = () => {
|
||||||
offset: number,
|
let total = 0;
|
||||||
limit: number,
|
let initialLoad = true;
|
||||||
projectId = '',
|
|
||||||
searchValue = '',
|
|
||||||
options: SWRConfiguration = {},
|
|
||||||
): IUseFeatureSearchOutput => {
|
|
||||||
const { KEY, fetcher } = getFeatureSearchFetcher(
|
|
||||||
projectId,
|
|
||||||
offset,
|
|
||||||
limit,
|
|
||||||
searchValue,
|
|
||||||
);
|
|
||||||
const { data, error, mutate } = useSWR<IFeatureSearchResponse>(
|
|
||||||
KEY,
|
|
||||||
fetcher,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
const refetch = useCallback(() => {
|
return (
|
||||||
mutate();
|
offset: number,
|
||||||
}, [mutate]);
|
limit: number,
|
||||||
|
projectId = '',
|
||||||
|
searchValue = '',
|
||||||
|
options: SWRConfiguration = {},
|
||||||
|
): IUseFeatureSearchOutput => {
|
||||||
|
const { KEY, fetcher } = getFeatureSearchFetcher(
|
||||||
|
projectId,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
searchValue,
|
||||||
|
);
|
||||||
|
const { data, error, mutate, isLoading } =
|
||||||
|
useSWR<IFeatureSearchResponse>(KEY, fetcher, options);
|
||||||
|
|
||||||
const returnData = data || fallbackData;
|
const refetch = useCallback(() => {
|
||||||
return {
|
mutate();
|
||||||
...returnData,
|
}, [mutate]);
|
||||||
loading: false,
|
|
||||||
error,
|
if (data?.total) {
|
||||||
refetch,
|
total = data.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoading && initialLoad) {
|
||||||
|
initialLoad = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const returnData = data || fallbackData;
|
||||||
|
return {
|
||||||
|
...returnData,
|
||||||
|
loading: isLoading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
total,
|
||||||
|
initialLoad: isLoading && initialLoad,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useFeatureSearch = createFeatureSearch();
|
||||||
|
|
||||||
const getFeatureSearchFetcher = (
|
const getFeatureSearchFetcher = (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
offset: number,
|
offset: number,
|
||||||
|
@ -12,7 +12,7 @@ const useLoading = (loading: boolean, selector = '[data-loading=true]') => {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
element.classList.add('skeleton');
|
element.classList.add('skeleton');
|
||||||
} else {
|
} else {
|
||||||
element.classList.remove('skeleton');
|
setTimeout(() => element.classList.remove('skeleton'), 10);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ button {
|
|||||||
.skeleton {
|
.skeleton {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
z-index: 9999;
|
z-index: 9990;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
fill: none;
|
fill: none;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
Loading…
Reference in New Issue
Block a user