Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,6 @@ export function FilterPanel({
padding-left: ${props => props.theme.space[2]}px;
padding-right: ${props => props.theme.space[2]}px;
height: 22px;
width: 128px;
`}
onClick={() => setExpandAllLabels(!expandAllLabels)}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ import {
LabelsViewMode,
} from 'shared/services/unifiedResourcePreferences';

import { UnifiedResources, useUnifiedResourcesFetch } from './UnifiedResources';
import { makeErrorAttempt, makeProcessingAttempt } from 'shared/hooks/useAsync';

import {
SharedUnifiedResource,
UnifiedResourcesPinning,
UnifiedResourcesQueryParams,
} from './types';
UnifiedResources,
useUnifiedResourcesFetch,
UnifiedResourcesProps,
} from './UnifiedResources';
import { SharedUnifiedResource, UnifiedResourcesQueryParams } from './types';

export default {
title: 'Shared/UnifiedResources',
Expand Down Expand Up @@ -75,14 +77,13 @@ const story = ({
updateClusterPinnedResources: async () => undefined,
},
params,
...props
}: {
fetchFunc: (
params: UrlResourcesParams,
signal: AbortSignal
) => Promise<ResourcesResponse<SharedUnifiedResource['resource']>>;
pinning?: UnifiedResourcesPinning;
params?: Partial<UnifiedResourcesQueryParams>;
}) => {
} & Omit<Partial<UnifiedResourcesProps>, 'fetchResources'>) => {
const mergedParams: UnifiedResourcesQueryParams = {
...{
sort: {
Expand Down Expand Up @@ -139,6 +140,7 @@ const story = ({
ActionButton: <ButtonBorder size="small">Connect</ButtonBorder>,
},
}))}
{...props}
/>
);
};
Expand Down Expand Up @@ -180,13 +182,13 @@ export const LoadingAfterScrolling = story({
},
});

export const Errored = story({
export const Failed = story({
fetchFunc: async () => {
throw new Error('Failed to fetch');
},
});

export const ErroredAfterScrolling = story({
export const FailedAfterScrolling = story({
fetchFunc: async params => {
if (params.startKey === 'next-key') {
throw new Error('Failed to fetch');
Expand All @@ -195,6 +197,22 @@ export const ErroredAfterScrolling = story({
},
});

export const FailedToLoadPreferences = story({
fetchFunc: async () => ({
agents: allResources,
}),
unifiedResourcePreferencesAttempt: makeErrorAttempt(
new Error('Network error')
),
});

export const LoadingPreferences = story({
fetchFunc: async () => ({
agents: allResources,
}),
unifiedResourcePreferencesAttempt: makeProcessingAttempt(),
});

export const PinningNotSupported = story({
fetchFunc: async () => {
return { agents: allResources, startKey: 'next-key' };
Expand Down
211 changes: 126 additions & 85 deletions web/packages/shared/components/UnifiedResources/UnifiedResources.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { useEffect, useState, useCallback } from 'react';
import React, {
useEffect,
useState,
useCallback,
Children,
PropsWithChildren,
} from 'react';

import styled from 'styled-components';
import {
Expand Down Expand Up @@ -44,6 +50,7 @@ import {
makeSuccessAttempt,
useAsync,
Attempt as AsyncAttempt,
hasFinished,
} from 'shared/hooks/useAsync';
import {
useKeyBasedPagination,
Expand Down Expand Up @@ -115,7 +122,7 @@ export type FilterKind = {
disabled: boolean;
};

interface UnifiedResourcesProps {
export interface UnifiedResourcesProps {
params: UnifiedResourcesQueryParams;
resourcesFetchAttempt: Attempt;
fetchResources(options?: { force?: boolean }): Promise<void>;
Expand All @@ -136,6 +143,13 @@ interface UnifiedResourcesProps {
setParams(params: UnifiedResourcesQueryParams): void;
/** A list of actions that can be performed on the selected items. */
bulkActions?: BulkAction[];
/**
* It is an attempt for initial fetch of preferences.
* When it is in progress, the component shows loading skeleton.
* Used only in Connect, where we fetch user preferences
* while the unified resources component is visible.
*/
unifiedResourcePreferencesAttempt?: AsyncAttempt<void>;
unifiedResourcePreferences: UnifiedResourcePreferences;
updateUnifiedResourcesPreferences(
preferences: UnifiedResourcePreferences
Expand All @@ -151,8 +165,9 @@ export function UnifiedResources(props: UnifiedResourcesProps) {
fetchResources,
availableKinds,
pinning,
unifiedResourcePreferences,
unifiedResourcePreferencesAttempt,
updateUnifiedResourcesPreferences,
unifiedResourcePreferences,
bulkActions = [],
} = props;

Expand Down Expand Up @@ -352,38 +367,39 @@ export function UnifiedResources(props: UnifiedResourcesProps) {
margin: 0 auto;
`}
>
{resourcesFetchAttempt.status === 'failed' && (
<ErrorBox>
{/* If pinning is hidden, we hide the different tabs to select a view (All resources, pinning).
This causes this error box to cover the search bar. If pinning isn't supported, we push down the
error by 60px to not hide the search bar.
*/}
<ErrorBoxInternal
topPadding={pinning.kind === 'hidden' ? '60px' : '0px'}
>
<Danger>
{resourcesFetchAttempt.statusText}
{/* we don't want them to try another request with BAD REQUEST, it will just fail again. */}
{resourcesFetchAttempt.statusCode !== 400 &&
resourcesFetchAttempt.statusCode !== 403 && (
<Box flex="0 0 auto" ml={2}>
<ButtonLink onClick={onRetryClicked}>Retry</ButtonLink>
</Box>
)}
</Danger>
</ErrorBoxInternal>
</ErrorBox>
)}
{getPinnedResourcesAttempt.status === 'error' && (
<ErrorBox>
<Danger>{getPinnedResourcesAttempt.statusText}</Danger>
</ErrorBox>
)}
{updatePinnedResourcesAttempt.status === 'error' && (
<ErrorBox>
<Danger>{updatePinnedResourcesAttempt.statusText}</Danger>
</ErrorBox>
)}
<ErrorsContainer>
{resourcesFetchAttempt.status === 'failed' && (
<Danger mb={0}>
Could not fetch resources: {resourcesFetchAttempt.statusText}
{/* we don't want them to try another request with BAD REQUEST, it will just fail again. */}
{resourcesFetchAttempt.statusCode !== 400 &&
resourcesFetchAttempt.statusCode !== 403 && (
<Box flex="0 0 auto" ml={2}>
<ButtonLink onClick={onRetryClicked}>Retry</ButtonLink>
</Box>
)}
</Danger>
)}
{getPinnedResourcesAttempt.status === 'error' && (
<Danger mb={0}>
Could not fetch pinned resources:{' '}
{getPinnedResourcesAttempt.statusText}
</Danger>
)}
{updatePinnedResourcesAttempt.status === 'error' && (
<Danger mb={0}>
Could not update pinned resources:{' '}
{updatePinnedResourcesAttempt.statusText}
</Danger>
)}
{unifiedResourcePreferencesAttempt?.status === 'error' && (
<Danger mb={0}>
Could not fetch unified view preferences:{' '}
{unifiedResourcePreferencesAttempt.statusText}
</Danger>
)}
</ErrorsContainer>

{props.Header}
<FilterPanel
params={params}
Expand Down Expand Up @@ -458,47 +474,68 @@ export function UnifiedResources(props: UnifiedResourcesProps) {
{pinning.kind === 'not-supported' && params.pinnedOnly ? (
<PinningNotSupported />
) : (
<ViewComponent
onLabelClick={label =>
setParams({
...params,
search: '',
query: makeAdvancedSearchQueryForLabel(label, params),
})
}
pinnedResources={pinnedResources}
selectedResources={selectedResources}
onSelectResource={handleSelectResource}
onPinResource={handlePinResource}
pinningSupport={getResourcePinningSupport(
pinning.kind,
updatePinnedResourcesAttempt
)}
isProcessing={
resourcesFetchAttempt.status === 'processing' ||
getPinnedResourcesAttempt.status === 'processing'
}
mappedResources={resources.map(unifiedResource => ({
item: mapResourceToViewItem(unifiedResource),
key: generateUnifiedResourceKey(unifiedResource.resource),
}))}
expandAllLabels={expandAllLabels}
/>
)}
<div ref={setTrigger} />
<ListFooter>
{resourcesFetchAttempt.status === 'failed' && resources.length > 0 && (
<ButtonSecondary onClick={onRetryClicked}>Load more</ButtonSecondary>
)}
{noResults && isSearchEmpty && !params.pinnedOnly && props.NoResources}
{noResults && params.pinnedOnly && isSearchEmpty && <NoPinned />}
{noResults && !isSearchEmpty && (
<NoResults
isPinnedTab={params.pinnedOnly}
query={params?.query || params?.search}
<>
<ViewComponent
onLabelClick={label =>
setParams({
...params,
search: '',
query: makeAdvancedSearchQueryForLabel(label, params),
})
}
pinnedResources={pinnedResources}
selectedResources={selectedResources}
onSelectResource={handleSelectResource}
onPinResource={handlePinResource}
pinningSupport={getResourcePinningSupport(
pinning.kind,
updatePinnedResourcesAttempt
)}
isProcessing={
// we don't check for '' in resourcesFetchAttempt because
// `keyBasedPagination` returns to that status on abort errors.
resourcesFetchAttempt.status === 'processing' ||
getPinnedResourcesAttempt.status === '' ||
getPinnedResourcesAttempt.status === 'processing' ||
unifiedResourcePreferencesAttempt?.status === '' ||
unifiedResourcePreferencesAttempt?.status === 'processing'
}
mappedResources={
// Hide the resources until the preferences are fetched.
// ViewComponent supports infinite scroll, so it shows both already loaded resources
// and a loading indicator if needed.
!unifiedResourcePreferencesAttempt ||
hasFinished(unifiedResourcePreferencesAttempt)
? resources.map(unifiedResource => ({
item: mapResourceToViewItem(unifiedResource),
key: generateUnifiedResourceKey(unifiedResource.resource),
}))
: []
}
expandAllLabels={expandAllLabels}
/>
)}
</ListFooter>
<div ref={setTrigger} />
<ListFooter>
{resourcesFetchAttempt.status === 'failed' &&
resources.length > 0 && (
<ButtonSecondary onClick={onRetryClicked}>
Load more
</ButtonSecondary>
)}
{noResults &&
isSearchEmpty &&
!params.pinnedOnly &&
props.NoResources}
{noResults && params.pinnedOnly && isSearchEmpty && <NoPinned />}
{noResults && !isSearchEmpty && (
<NoResults
isPinnedTab={params.pinnedOnly}
query={params?.query || params?.search}
/>
)}
</ListFooter>
</>
)}
</div>
);
}
Expand Down Expand Up @@ -601,20 +638,24 @@ function NoResults({
return null;
}

const ErrorBox = styled(Box)`
function ErrorsContainer(props: PropsWithChildren<unknown>) {
if (!Children.toArray(props.children).length) {
return null;
}

return <ErrorBox>{props.children}</ErrorBox>;
}

const ErrorBox = styled(Flex)`
position: sticky;
top: 0;
flex-direction: column;
top: ${props => props.theme.space[3]}px;
gap: ${props => props.theme.space[1]}px;
padding-top: ${props => props.theme.space[1]}px;
padding-bottom: ${props => props.theme.space[3]}px;
z-index: 1;
`;

const ErrorBoxInternal = styled(Box)`
position: absolute;
left: 0;
right: 0;
top: ${props => props.topPadding};
margin: ${props => props.theme.space[1]}px 10% 0 10%;
`;

const INDICATOR_SIZE = '48px';

// It's important to make the footer at least as big as the loading indicator,
Expand Down