Skip to content

Commit

Permalink
feat(🎸): Add ResolvedQuery, LoadingEmptyState, and Tooltip (#80)
Browse files Browse the repository at this point in the history
* feat: 🎸 Add ResolvedQuery, LoadingEmptyState, and Tooltip

This adds lib-ui candidates from the forklift-ui repo

* Remove conditional tooltip & update results array to map

* Change name from result(s)Map nto result(s)WithErrorTitle(s)
  • Loading branch information
ibolton336 authored Oct 15, 2021
1 parent 126abe8 commit ed50b7e
Show file tree
Hide file tree
Showing 10 changed files with 306 additions and 8 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
},
"devDependencies": {
"@babel/core": "^7.13.16",
"@patternfly/react-core": "^4.115.1",
"@patternfly/react-core": "^4.157.3",
"@patternfly/react-table": "^4.26.6",
"@patternfly/react-tokens": "^4.11.2",
"@rollup/plugin-commonjs": "^18.0.0",
Expand Down Expand Up @@ -90,6 +90,7 @@
},
"dependencies": {
"fast-deep-equal": "^3.1.3",
"react-query": "^3.26.0",
"yup": "^0.32.9"
}
}
13 changes: 13 additions & 0 deletions src/common/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { UseMutationResult, UseQueryResult } from 'react-query';

export type UnknownResult = Pick<
UseQueryResult<unknown>,
'isError' | 'isLoading' | 'isIdle' | 'error'
>;

export type UnknownMutationResult = Pick<
UseMutationResult<unknown>,
'isError' | 'isLoading' | 'isIdle' | 'error' | 'reset'
>;

export type ResultsWithErrorTitles = { result: UnknownResult; errorTitle: string };
35 changes: 35 additions & 0 deletions src/components/LoadingEmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
Bullseye,
EmptyState,
EmptyStateBody,
Spinner,
SpinnerProps,
Title,
} from '@patternfly/react-core';
import * as React from 'react';

interface ILoadingEmptyStateProps {
className?: string;
spinnerProps?: Partial<SpinnerProps>;
body?: React.ReactNode;
}

const LoadingEmptyState: React.FunctionComponent<ILoadingEmptyStateProps> = ({
className = '',
spinnerProps = {},
body = null,
}: ILoadingEmptyStateProps) => (
<Bullseye className={className}>
<EmptyState variant="large">
<div className="pf-c-empty-state__icon">
<Spinner aria-labelledby="loadingPrefLabel" size="xl" {...spinnerProps} />
</div>
<Title id="loadingPrefLabel" headingLevel="h2">
Loading...
</Title>
{body ? <EmptyStateBody>{body}</EmptyStateBody> : null}
</EmptyState>
</Bullseye>
);

export default LoadingEmptyState;
106 changes: 106 additions & 0 deletions src/components/ResolvedQuery/ResolvedQueries.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import * as React from 'react';
import { UseMutationResult } from 'react-query';
import {
Spinner,
Alert,
AlertActionCloseButton,
SpinnerProps,
AlertProps,
AlertGroup,
} from '@patternfly/react-core';
import spacing from '@patternfly/react-styles/css/utilities/Spacing/spacing';
import { getAggregateQueryStatus } from '../../queries/helpers';
import { ResultsWithErrorTitles } from '../../common/types';
import { KubeClientError } from '../../modules/kube-client/types';
import LoadingEmptyState from '../LoadingEmptyState';

export type QuerySpinnerMode = 'inline' | 'emptyState' | 'none';

export interface IResolvedQueriesProps {
resultsWithErrorTitles: ResultsWithErrorTitles[];
errorsInline?: boolean;
spinnerMode?: QuerySpinnerMode;
emptyStateBody?: React.ReactNode;
spinnerProps?: Partial<SpinnerProps>;
alertProps?: Partial<AlertProps>;
className?: string;
forceLoadingState?: boolean;
children?: React.ReactNode;
}

export const ResolvedQueries: React.FunctionComponent<IResolvedQueriesProps> = ({
resultsWithErrorTitles,
errorsInline = true,
spinnerMode = 'emptyState',
emptyStateBody = null,
spinnerProps = {},
alertProps = {},
className = '',
forceLoadingState = false,
children = null,
}: IResolvedQueriesProps) => {
const status = getAggregateQueryStatus(resultsWithErrorTitles.map((r) => r.result));
const erroredResults = resultsWithErrorTitles.filter(
(resultWithErrorTitle) => resultWithErrorTitle.result.isError
);
let spinner: React.ReactNode = null;
if (spinnerMode === 'inline') {
spinner = <Spinner size="lg" className={className} {...spinnerProps} />;
} else if (spinnerMode === 'emptyState') {
spinner = <LoadingEmptyState spinnerProps={spinnerProps} body={emptyStateBody} />;
}

return (
<>
{status === 'loading' || forceLoadingState ? (
spinner
) : status === 'error' ? (
<AlertGroup aria-live="assertive">
{erroredResults.map((resultWithErrorTitle, index) => {
const { result, errorTitle } = resultWithErrorTitle;
return (
<Alert
key={`error-${index}`}
variant="danger"
isInline={errorsInline}
title={errorTitle}
className={`${
index !== erroredResults.length - 1 ? spacing.mbMd : ''
} ${className}`}
actionClose={
(result as { reset?: () => void }).reset ? (
<AlertActionCloseButton
aria-label="Dismiss error"
onClose={(result as UseMutationResult<unknown>).reset}
/>
) : null
}
{...alertProps}
>
{result.error ? (
<>
{(result.error as Error).message || null}
{(result.error as KubeClientError).response ? (
<>
<br />
{(result.error as KubeClientError).response?.data?.message}
</>
) : null}
{(result.error as Response).status
? `${(result.error as Response).status}: ${
(result.error as Response).statusText
}`
: null}
{typeof result.error === 'string' ? result.error : null}
</>
) : null}
</Alert>
);
})}
</AlertGroup>
) : (
children
)}
</>
);
};
16 changes: 16 additions & 0 deletions src/components/ResolvedQuery/ResolvedQuery.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react';
import { UnknownResult } from '../../common/types';
import { ResolvedQueries, IResolvedQueriesProps } from './ResolvedQueries';

export interface IResolvedQueryProps extends Omit<IResolvedQueriesProps, 'resultsWithErrorTitles'> {
result: UnknownResult;
errorTitle: string;
}

export const ResolvedQuery: React.FunctionComponent<IResolvedQueryProps> = ({
result,
errorTitle,
...props
}: IResolvedQueryProps) => (
<ResolvedQueries {...props} resultsWithErrorTitles={[{ result, errorTitle }]} />
);
2 changes: 2 additions & 0 deletions src/components/ResolvedQuery/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './ResolvedQueries';
export * from './ResolvedQuery';
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export * from './components/StatusIcon';
export * from './components/ValidatedTextInput';
export * from './components/ResolvedQuery';
export * from './components/LoadingEmptyState';

export * from './hooks/useSelectionState';
export * from './hooks/useFormState';
Expand Down
3 changes: 3 additions & 0 deletions src/modules/kube-client/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { AxiosError } from 'axios';

export type KubeClientError = AxiosError<{ message: string }>;
9 changes: 9 additions & 0 deletions src/queries/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { QueryStatus } from 'react-query';
import { UnknownResult } from '../common/types';

export const getAggregateQueryStatus = (queryResults: UnknownResult[]): QueryStatus => {
if (queryResults.some((result) => result.isError)) return 'error';
if (queryResults.some((result) => result.isLoading)) return 'loading';
if (queryResults.every((result) => result.isIdle)) return 'idle';
return 'success';
};
Loading

0 comments on commit ed50b7e

Please sign in to comment.