Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Status component to @console/shared #1697

Merged
merged 3 commits into from
Jul 18, 2019
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 @@ -94,8 +94,9 @@ describe(ClusterServiceVersionTableRow.displayName, () => {

it('renders column for app status', () => {
const col = wrapper.find(TableRow).childAt(3);

expect(col.childAt(0).text()).toContain(CSVConditionReason.CSVReasonInstallSuccessful);
const statusComponent = col.childAt(0).find('SuccessStatus');
expect(statusComponent.exists()).toBeTruthy();
expect(statusComponent.prop('title')).toEqual(CSVConditionReason.CSVReasonInstallSuccessful);
});

it('renders "disabling" status if CSV has `deletionTimestamp`', () => {
Expand Down
16 changes: 10 additions & 6 deletions frontend/__tests__/components/route-pages.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,9 @@ describe(RouteStatus.displayName, () => {
};

const wrapper = mount(<RouteStatus obj={route} />);
expect(wrapper.find('.co-icon-and-text__icon').exists()).toBe(true);
expect(wrapper.text()).toEqual('Accepted');
const statusComponent = wrapper.find('SuccessStatus');
expect(statusComponent.exists()).toBeTruthy();
expect(statusComponent.prop('title')).toEqual('Accepted');
});

it('renders Rejected status', () => {
Expand All @@ -223,8 +224,9 @@ describe(RouteStatus.displayName, () => {
};

const wrapper = mount(<RouteStatus obj={route} />);
expect(wrapper.find('.co-icon-and-text__icon').exists()).toBe(true);
expect(wrapper.text()).toEqual('Rejected');
const statusComponent = wrapper.find('ErrorStatus');
expect(statusComponent.exists()).toBeTruthy();
expect(statusComponent.prop('title')).toEqual('Rejected');
});

it('renders Pending status', () => {
Expand All @@ -237,7 +239,9 @@ describe(RouteStatus.displayName, () => {
};

const wrapper = mount(<RouteStatus obj={route} />);
expect(wrapper.find('.co-icon-and-text__icon').exists()).toBe(true);
expect(wrapper.text()).toEqual('Pending');
const statusComponent = wrapper.find('StatusIconAndText');
const icon = wrapper.find('HourglassHalfIcon');
expect(icon.exists()).toBeTruthy();
expect(statusComponent.prop('title')).toEqual('Pending');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Progress } from '@patternfly/react-core';

import './data-resiliency.scss';

import { GreenCheckCircleIcon, RedExclamationCircleIcon } from '@console/shared';
import { DashboardCard } from '@console/internal/components/dashboard/dashboard-card/card';
import { DashboardCardBody } from '@console/internal/components/dashboard/dashboard-card/card-body';
import { DashboardCardHeader } from '@console/internal/components/dashboard/dashboard-card/card-header';
Expand All @@ -12,10 +13,6 @@ import {
withDashboardResources,
DashboardItemProps,
} from '@console/internal/components/dashboards-page/with-dashboard-resources';
import {
GreenCheckCircleIcon,
RedExclamationCircleIcon,
} from '@console/internal/components/utils/status-icon';

import { DATA_RESILIENCY_QUERIES } from '../../../../constants/queries';

Expand Down
1 change: 1 addition & 0 deletions frontend/packages/console-shared/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default as TechPreviewBadge } from './TechPreviewBadge';
export * from './status';
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as React from 'react';

import PopoverStatus from './PopoverStatus';
import StatusIconAndText from './StatusIconAndText';
import { RedExclamationCircleIcon } from './Icons';

type ErrorStatusProps = {
title?: string;
iconOnly?: boolean;
};

const ErrorStatus: React.FC<ErrorStatusProps> = ({ title, iconOnly, children }) => {
const icon = <RedExclamationCircleIcon />;
return children ? (
<PopoverStatus icon={icon} title={title} iconOnly={iconOnly}>
{children}
</PopoverStatus>
) : (
<StatusIconAndText icon={icon} title={title} iconOnly={iconOnly} />
);
};

export default ErrorStatus;
27 changes: 27 additions & 0 deletions frontend/packages/console-shared/src/components/status/Icons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from 'react';
import {
CheckCircleIcon,
ExclamationCircleIcon,
ExclamationTriangleIcon,
} from '@patternfly/react-icons';
import {
global_success_color_100 as successColor,
global_warning_color_100 as warningColor,
global_danger_color_100 as dangerColor,
} from '@patternfly/react-tokens';

export type ColoredIconProps = {
className?: string;
};

export const GreenCheckCircleIcon: React.FC<ColoredIconProps> = ({ className }) => {
return <CheckCircleIcon color={successColor.value} className={className} />;
};

export const RedExclamationCircleIcon: React.FC<ColoredIconProps> = ({ className }) => {
return <ExclamationCircleIcon color={dangerColor.value} className={className} />;
};

export const YellowExclamationTriangleIcon: React.FC<ColoredIconProps> = ({ className }) => {
return <ExclamationTriangleIcon color={warningColor.value} className={className} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as React from 'react';
import { InfoCircleIcon } from '@patternfly/react-icons';

import PopoverStatus from './PopoverStatus';
import StatusIconAndText from './StatusIconAndText';

type InfoStatusProps = {
title?: string;
iconOnly?: boolean;
};

const InfoStatus: React.FC<InfoStatusProps> = ({ title, iconOnly, children }) => {
const icon = <InfoCircleIcon />;
return children ? (
<PopoverStatus icon={icon} title={title} iconOnly={iconOnly}>
{children}
</PopoverStatus>
) : (
<StatusIconAndText icon={icon} title={title} iconOnly={iconOnly} />
);
};

export default InfoStatus;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as React from 'react';
import * as History from 'history';
import { Link } from 'react-router-dom';

import StatusIconAndText from './StatusIconAndText';

type LinkStatusProps = React.ComponentProps<typeof StatusIconAndText> & {
linkTitle?: string;
linkTo?: History.LocationDescriptor;
};

const LinkStatus: React.FC<LinkStatusProps> = ({
icon,
title,
spin,
linkTitle,
linkTo,
iconOnly,
}) =>
linkTo ? (
<Link to={linkTo} title={linkTitle}>
<StatusIconAndText icon={icon} title={title} spin={spin} iconOnly={iconOnly} />
</Link>
) : (
<StatusIconAndText icon={icon} title={title} spin={spin} iconOnly={iconOnly} />
);

export default LinkStatus;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as React from 'react';
import { Popover, PopoverPosition } from '@patternfly/react-core';
import { Button } from 'patternfly-react';

import StatusIconAndText from './StatusIconAndText';

const PopoverStatus: React.FC<React.ComponentProps<typeof StatusIconAndText>> = ({
icon,
title,
spin,
children,
iconOnly,
}) => (
<Popover position={PopoverPosition.right} headerContent={title} bodyContent={children}>
<Button bsStyle="link">
<StatusIconAndText icon={icon} title={title} spin={spin} iconOnly={iconOnly} />
</Button>
</Popover>
);

export default PopoverStatus;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from 'react';
import { InProgressIcon } from '@patternfly/react-icons';

import StatusIconAndText from './StatusIconAndText';

type ProgressStatusProps = {
title?: string;
iconOnly?: boolean;
};

const ProgressStatus: React.FC<ProgressStatusProps> = ({ title, iconOnly }) => (
<StatusIconAndText icon={<InProgressIcon />} title={title} iconOnly={iconOnly} />
);

export default ProgressStatus;
82 changes: 82 additions & 0 deletions frontend/packages/console-shared/src/components/status/Status.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as React from 'react';
import {
HourglassStartIcon,
HourglassHalfIcon,
SyncAltIcon,
BanIcon,
ExclamationTriangleIcon,
UnknownIcon,
} from '@patternfly/react-icons';

import StatusIconAndText from './StatusIconAndText';
import ProgressStatus from './ProgressStatus';
import ErrorStatus from './ErrorStatus';
import SuccessStatus from './SuccessStatus';
import InfoStatus from './InfoStatus';
import { DASH } from '../../constants';

export type StatusProps = {
status?: string;
title?: string;
iconOnly?: boolean;
};

const Status: React.FC<StatusProps> = ({ status, title, children, iconOnly }) => {
const statusProps = { title: title || status, iconOnly };
switch (status) {
case 'New':
return <StatusIconAndText {...statusProps} icon={<HourglassStartIcon />} />;

case 'Pending':
return <StatusIconAndText {...statusProps} icon={<HourglassHalfIcon />} />;

case 'ContainerCreating':
return <ProgressStatus {...statusProps} />;

case 'In Progress':
case 'Running':
case 'Updating':
case 'Upgrading':
return <StatusIconAndText {...statusProps} icon={<SyncAltIcon />} />;

case 'Cancelled':
case 'Expired':
case 'Not Ready':
case 'Terminating':
return <StatusIconAndText {...statusProps} icon={<BanIcon />} />;

case 'Warning':
return <StatusIconAndText {...statusProps} icon={<ExclamationTriangleIcon />} />;

case 'ContainerCannotRun':
case 'CrashLoopBackOff':
case 'Critical':
case 'Error':
case 'Failed':
case 'InstallCheckFailed':
case 'Lost':
case 'Rejected':
return <ErrorStatus {...statusProps}>{children}</ErrorStatus>;

case 'Accepted':
case 'Active':
case 'Bound':
case 'Complete':
case 'Completed':
case 'Enabled':
case 'Ready':
case 'Up to date':
return <SuccessStatus {...statusProps} />;

case 'Info':
return <InfoStatus {...statusProps}>{children}</InfoStatus>;

case 'Unknown':
return <StatusIconAndText {...statusProps} icon={<UnknownIcon />} />;

default:
return <>{DASH}</>;
}
};

export default Status;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as React from 'react';
import Status from './Status';

const StatusIcon = ({ status }) => <Status status={status} iconOnly />;

export default StatusIcon;
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as React from 'react';
import classNames from 'classnames';

import { CamelCaseWrap } from '@console/internal/components/utils';
import { DASH } from '../../constants';

type StatusIconAndTextProps = {
icon?: React.ReactElement;
title?: string;
spin?: boolean;
iconOnly?: boolean;
};

const StatusIconAndText: React.FC<StatusIconAndTextProps> = ({ icon, title, spin, iconOnly }) => {
if (!title) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering, if title isn't defined but icon is, we'll still render a dash - is this an expected behavior?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not, I'll revisit.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is intentional as it allows to handle the case when StatusIconAndText component is used and the status (title prop) is not yet set. There is explicit iconOnly prop to render an icon without text.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, thanks for clarification.

return <>{DASH}</>;
}

return (
<span className="co-icon-and-text" title={title}>
{icon &&
React.cloneElement(icon, {
className: classNames(
spin && 'fa-spin',
icon.props.className,
!iconOnly && 'co-icon-and-text__icon',
),
})}
{!iconOnly && <CamelCaseWrap value={title} />}
</span>
);
};

export default StatusIconAndText;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from 'react';

import { GreenCheckCircleIcon } from './Icons';
import StatusIconAndText from './StatusIconAndText';

type SuccessStatusProps = {
title?: string;
iconOnly?: boolean;
};

const SuccessStatus: React.FC<SuccessStatusProps> = ({ title, iconOnly }) => (
<StatusIconAndText icon={<GreenCheckCircleIcon />} title={title} iconOnly={iconOnly} />
);

export default SuccessStatus;
10 changes: 10 additions & 0 deletions frontend/packages/console-shared/src/components/status/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export { default as Status } from './Status';
export { default as StatusIcon } from './StatusIcon';
export { default as StatusIconAndText } from './StatusIconAndText';
export { default as PopoverStatus } from './PopoverStatus';
export { default as LinkStatus } from './LinkStatus';
export { default as SuccessStatus } from './SuccessStatus';
export { default as ErrorStatus } from './ErrorStatus';
export { default as InfoStatus } from './InfoStatus';
export { default as ProgressStatus } from './ProgressStatus';
export * from './Icons';
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import * as React from 'react';

import { Status } from '@console/shared';
import { TableRow, TableData } from '@console/internal/components/factory';
import {
Kebab,
ResourceLink,
Timestamp,
ResourceKebab,
StatusIconAndText,
} from '@console/internal/components/utils';
import { Kebab, ResourceLink, Timestamp, ResourceKebab } from '@console/internal/components/utils';
import { referenceForModel } from '@console/internal/module/k8s';
import { pipelineRunFilterReducer } from '../../utils/pipeline-filter-reducer';
import { reRunPipelineRun, stopPipelineRun } from '../../utils/pipeline-actions';
Expand Down Expand Up @@ -40,7 +36,7 @@ const PipelineRunRow: React.FC<PipelineRunRowProps> = ({ obj, index, key, style
<Timestamp timestamp={obj.status && obj.status.startTime} />
</TableData>
<TableData className={tableColumnClasses[2]}>
<StatusIconAndText status={pipelineRunFilterReducer(obj)} />
<Status status={pipelineRunFilterReducer(obj)} />
</TableData>
<TableData className={tableColumnClasses[3]}>
<PipelineTaskStatus pipelinerun={obj} />
Expand Down
Loading