diff --git a/web/packages/shared/components/UnifiedResources/FilterPanel.tsx b/web/packages/shared/components/UnifiedResources/FilterPanel.tsx index 1f09826f24ffb..c15ca772582ad 100644 --- a/web/packages/shared/components/UnifiedResources/FilterPanel.tsx +++ b/web/packages/shared/components/UnifiedResources/FilterPanel.tsx @@ -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)} > diff --git a/web/packages/shared/components/UnifiedResources/UnifiedResources.story.tsx b/web/packages/shared/components/UnifiedResources/UnifiedResources.story.tsx index 4dc187b9bd5ba..1b183082571b0 100644 --- a/web/packages/shared/components/UnifiedResources/UnifiedResources.story.tsx +++ b/web/packages/shared/components/UnifiedResources/UnifiedResources.story.tsx @@ -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', @@ -75,14 +77,13 @@ const story = ({ updateClusterPinnedResources: async () => undefined, }, params, + ...props }: { fetchFunc: ( params: UrlResourcesParams, signal: AbortSignal ) => Promise>; - pinning?: UnifiedResourcesPinning; - params?: Partial; -}) => { +} & Omit, 'fetchResources'>) => { const mergedParams: UnifiedResourcesQueryParams = { ...{ sort: { @@ -139,6 +140,7 @@ const story = ({ ActionButton: Connect, }, }))} + {...props} /> ); }; @@ -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'); @@ -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' }; diff --git a/web/packages/shared/components/UnifiedResources/UnifiedResources.tsx b/web/packages/shared/components/UnifiedResources/UnifiedResources.tsx index 014bfcfb8bc4e..310fd15f6b957 100644 --- a/web/packages/shared/components/UnifiedResources/UnifiedResources.tsx +++ b/web/packages/shared/components/UnifiedResources/UnifiedResources.tsx @@ -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 { @@ -44,6 +50,7 @@ import { makeSuccessAttempt, useAsync, Attempt as AsyncAttempt, + hasFinished, } from 'shared/hooks/useAsync'; import { useKeyBasedPagination, @@ -115,7 +122,7 @@ export type FilterKind = { disabled: boolean; }; -interface UnifiedResourcesProps { +export interface UnifiedResourcesProps { params: UnifiedResourcesQueryParams; resourcesFetchAttempt: Attempt; fetchResources(options?: { force?: boolean }): Promise; @@ -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; unifiedResourcePreferences: UnifiedResourcePreferences; updateUnifiedResourcesPreferences( preferences: UnifiedResourcePreferences @@ -151,8 +165,9 @@ export function UnifiedResources(props: UnifiedResourcesProps) { fetchResources, availableKinds, pinning, - unifiedResourcePreferences, + unifiedResourcePreferencesAttempt, updateUnifiedResourcesPreferences, + unifiedResourcePreferences, bulkActions = [], } = props; @@ -352,38 +367,39 @@ export function UnifiedResources(props: UnifiedResourcesProps) { margin: 0 auto; `} > - {resourcesFetchAttempt.status === 'failed' && ( - - {/* 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. - */} - - - {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 && ( - - Retry - - )} - - - - )} - {getPinnedResourcesAttempt.status === 'error' && ( - - {getPinnedResourcesAttempt.statusText} - - )} - {updatePinnedResourcesAttempt.status === 'error' && ( - - {updatePinnedResourcesAttempt.statusText} - - )} + + {resourcesFetchAttempt.status === 'failed' && ( + + 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 && ( + + Retry + + )} + + )} + {getPinnedResourcesAttempt.status === 'error' && ( + + Could not fetch pinned resources:{' '} + {getPinnedResourcesAttempt.statusText} + + )} + {updatePinnedResourcesAttempt.status === 'error' && ( + + Could not update pinned resources:{' '} + {updatePinnedResourcesAttempt.statusText} + + )} + {unifiedResourcePreferencesAttempt?.status === 'error' && ( + + Could not fetch unified view preferences:{' '} + {unifiedResourcePreferencesAttempt.statusText} + + )} + + {props.Header} ) : ( - - 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} - /> - )} -
- - {resourcesFetchAttempt.status === 'failed' && resources.length > 0 && ( - Load more - )} - {noResults && isSearchEmpty && !params.pinnedOnly && props.NoResources} - {noResults && params.pinnedOnly && isSearchEmpty && } - {noResults && !isSearchEmpty && ( - + + 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} /> - )} - +
+ + {resourcesFetchAttempt.status === 'failed' && + resources.length > 0 && ( + + Load more + + )} + {noResults && + isSearchEmpty && + !params.pinnedOnly && + props.NoResources} + {noResults && params.pinnedOnly && isSearchEmpty && } + {noResults && !isSearchEmpty && ( + + )} + + + )}
); } @@ -601,20 +638,24 @@ function NoResults({ return null; } -const ErrorBox = styled(Box)` +function ErrorsContainer(props: PropsWithChildren) { + if (!Children.toArray(props.children).length) { + return null; + } + + return {props.children}; +} + +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,