Skip to content

Commit

Permalink
Cleanup StatusIcon component states (#12039)
Browse files Browse the repository at this point in the history
* Start cleanup of StatusIcon component and add unit test

* Update StatusIcon booleans to use single status prop

* Update AllConnectionStatusCell component to use configuration-based rendering.

* Cleanup code bsed on PR review
  • Loading branch information
edmundito authored Apr 18, 2022
1 parent ea61bf6 commit 6e075d1
Show file tree
Hide file tree
Showing 11 changed files with 154 additions and 142 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type TryAfterErrorBlockProps = {

const TryAfterErrorBlock: React.FC<TryAfterErrorBlockProps> = ({ message, onClick }) => (
<Block>
<StatusIcon success={false} big />
<StatusIcon big />
<Title center>{message || <FormattedMessage id="form.schemaFailed" />}</Title>
<AgainButton onClick={onClick} danger>
<FormattedMessage id="form.tryAgain" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,84 +2,51 @@ import React, { useMemo } from "react";
import { useIntl } from "react-intl";

import StatusIcon from "components/StatusIcon";
import { StatusIconStatus } from "components/StatusIcon/StatusIcon";

import { Status } from "../types";

type AllConnectionsStatusCellProps = {
connectEntities: {
name: string;
connector: string;
status: string;
lastSyncStatus: string;
}[];
};

const AllConnectionsStatusCell: React.FC<AllConnectionsStatusCellProps> = ({ connectEntities }) => {
const formatMessage = useIntl().formatMessage;

const active = useMemo(
() => connectEntities.filter((entity) => entity.lastSyncStatus === Status.ACTIVE),
[connectEntities]
);
const _statusConfig: { status: Status; statusIconStatus?: StatusIconStatus; titleId: string }[] = [
{ status: Status.ACTIVE, statusIconStatus: "success", titleId: "connection.successSync" },
{ status: Status.INACTIVE, statusIconStatus: "inactive", titleId: "connection.disabledConnection" },
{ status: Status.FAILED, titleId: "connection.failedSync" },
{ status: Status.EMPTY, statusIconStatus: "empty", titleId: "connection.noSyncData" },
];

const inactive = useMemo(
() => connectEntities.filter((entity) => entity.lastSyncStatus === Status.INACTIVE),
[connectEntities]
);
interface AllConnectionStatusConnectEntity {
name: string;
connector: string;
status: string;
lastSyncStatus: string;
}

const failed = useMemo(
() => connectEntities.filter((entity) => entity.lastSyncStatus === Status.FAILED),
[connectEntities]
);
interface AllConnectionsStatusCellProps {
connectEntities: AllConnectionStatusConnectEntity[];
}

const empty = useMemo(
() => connectEntities.filter((entity) => entity.lastSyncStatus === Status.EMPTY),
[connectEntities]
);

if (!connectEntities.length) {
return null;
}

return (
<>
{active.length ? (
<StatusIcon
success
value={active.length}
title={formatMessage({
id: "connection.successSync",
})}
/>
) : null}
{inactive.length ? (
<StatusIcon
inactive
value={inactive.length}
title={formatMessage({
id: "connection.disabledConnection",
})}
/>
) : null}
{failed.length ? (
<StatusIcon
value={failed.length}
title={formatMessage({
id: "connection.failedSync",
})}
/>
) : null}
{empty.length ? (
<StatusIcon
empty
value={empty.length}
title={formatMessage({
id: "connection.noSyncData",
})}
/>
) : null}
</>
);
const AllConnectionsStatusCell: React.FC<AllConnectionsStatusCellProps> = ({ connectEntities }) => {
const { formatMessage } = useIntl();

const statusIconProps = useMemo(() => {
if (connectEntities.length) {
for (const { status, statusIconStatus, titleId } of _statusConfig) {
const filteredEntities = connectEntities.filter((entity) => entity.lastSyncStatus === status);
if (filteredEntities.length) {
return {
status: statusIconStatus,
value: filteredEntities.length,
title: titleId,
};
}
}
}

return undefined;
}, [connectEntities]);

return statusIconProps ? (
<StatusIcon {...statusIconProps} title={formatMessage({ id: statusIconProps.title })} />
) : null;
};

export default AllConnectionsStatusCell;
25 changes: 14 additions & 11 deletions airbyte-webapp/src/components/EntityTable/components/NameCell.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from "react";
import React, { useMemo } from "react";
import styled from "styled-components";
import { useIntl } from "react-intl";

import StatusIcon from "components/StatusIcon";
import ImageBlock from "components/ImageBlock";
import { StatusIconStatus } from "components/StatusIcon/StatusIcon";

import { Status } from "../types";

Expand Down Expand Up @@ -41,6 +42,17 @@ const Image = styled(ImageBlock)`

const NameCell: React.FC<IProps> = ({ value, enabled, status, icon, img }) => {
const formatMessage = useIntl().formatMessage;
const statusIconStatus = useMemo<StatusIconStatus | undefined>(
() =>
status === Status.EMPTY
? "empty"
: status === Status.ACTIVE
? "success"
: status === Status.INACTIVE
? "inactive"
: undefined,
[status]
);
const title =
status === Status.EMPTY
? formatMessage({
Expand All @@ -60,16 +72,7 @@ const NameCell: React.FC<IProps> = ({ value, enabled, status, icon, img }) => {

return (
<Content>
{status ? (
<StatusIcon
title={title}
empty={status === Status.EMPTY}
success={status === Status.ACTIVE}
inactive={status === Status.INACTIVE}
/>
) : (
<Space />
)}
{status ? <StatusIcon title={title} status={statusIconStatus} /> : <Space />}
{icon && <Image small img={img} />}
<Name enabled={enabled}>{value}</Name>
</Content>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ const MainInfo: React.FC<IProps> = ({

const getIcon = () => {
if (isPartialSuccess) {
return <ErrorSign warning />;
return <ErrorSign status="warning" />;
} else if (isFailed && !shortInfo) {
return <ErrorSign />;
}
Expand Down
14 changes: 14 additions & 0 deletions airbyte-webapp/src/components/StatusIcon/PauseIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
interface Props {
color?: string;
title?: string;
}

const PauseIcon = ({ color = "currentColor", title }: Props): JSX.Element => (
<svg width="6" height="11" viewBox="0 0 6 11" fill="none" role="img" data-icon="pause">
{title && <title>{title}</title>}
<line x1="1" y1="1.5" x2="1" y2="10.5" stroke={color} strokeWidth="2" />
<line x1="5" y1="1.5" x2="5" y2="10.5" stroke={color} strokeWidth="2" />
</svg>
);

export default PauseIcon;
39 changes: 39 additions & 0 deletions airbyte-webapp/src/components/StatusIcon/StatusIcon.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { render } from "@testing-library/react";

import StatusIcon, { StatusIconStatus } from "./StatusIcon";

describe("<StatusIcon />", () => {
test("renders with title and default icon", () => {
const title = "Pulpo";
const value = 888;

const component = render(<StatusIcon title={title} value={value} />);

expect(component.getByTitle(title)).toBeDefined();
expect(component.getByRole("img")).toHaveAttribute("data-icon", "times");
expect(component.getByText(`${value}`)).toBeDefined();
});

const statusCases: { status: StatusIconStatus; icon: string }[] = [
{ status: "success", icon: "check" },
{ status: "inactive", icon: "pause" },
{ status: "empty", icon: "ban" },
{ status: "warning", icon: "exclamation-triangle" },
];

test.each(statusCases)("renders $status status", ({ status, icon }) => {
const title = `Status is ${status}`;
const value = 888;
const props = {
title,
value,
status,
};

const component = render(<StatusIcon {...props} />);

expect(component.getByTitle(title)).toBeDefined();
expect(component.getByRole("img")).toHaveAttribute("data-icon", icon);
expect(component.getByText(`${value}`)).toBeDefined();
});
});
77 changes: 35 additions & 42 deletions airbyte-webapp/src/components/StatusIcon/StatusIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,39 @@
import React from "react";
import styled from "styled-components";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCheck, faTimes, faBan, faExclamationTriangle } from "@fortawesome/free-solid-svg-icons";
import { faCheck, faTimes, faBan, faExclamationTriangle, IconDefinition } from "@fortawesome/free-solid-svg-icons";

import PauseIcon from "./components/Pause";
import PauseIcon from "./PauseIcon";

type IProps = {
success?: boolean;
warning?: boolean;
title?: string;
inactive?: boolean;
empty?: boolean;
export type StatusIconStatus = "empty" | "inactive" | "success" | "warning";

interface Props {
className?: string;
status?: StatusIconStatus;
title?: string;
big?: boolean;
value?: string | number;
};
}

const getWidth = (props: IProps) => {
if (props.big) {
return props.value ? 57 : 40;
}
const getBadgeWidth = (props: Props) => (props.big ? (props.value ? 57 : 40) : props.value ? 37 : 20);

return props.value ? 37 : 20;
const _iconByStatus: Partial<Record<StatusIconStatus, IconDefinition | undefined>> = {
empty: faBan,
success: faCheck,
warning: faExclamationTriangle,
};

const Badge = styled.div<IProps>`
width: ${(props) => getWidth(props)}px;
const _themeByStatus: Record<StatusIconStatus, string> = {
empty: "attentionColor",
inactive: "lightTextColor",
success: "successColor",
warning: "warningColor",
};

const Badge = styled.div<Props>`
width: ${(props) => getBadgeWidth(props)}px;
height: ${({ big }) => (big ? 40 : 20)}px;
background: ${(props) =>
props.success
? props.theme.successColor
: props.inactive
? props.theme.lightTextColor
: props.empty
? props.theme.attentionColor
: props.warning
? props.theme.warningColor
: props.theme.dangerColor};
background: ${(props) => props.theme[(props.status && _themeByStatus[props.status]) || "dangerColor"]};
box-shadow: 0 1px 2px ${({ theme }) => theme.shadowColor};
border-radius: ${({ value }) => (value ? "15px" : "50%")};
margin-right: 10px;
Expand All @@ -56,21 +53,17 @@ const Value = styled.span`
vertical-align: top;
`;

const StatusIcon: React.FC<IProps> = (props) => (
<Badge {...props}>
{props.success ? (
<FontAwesomeIcon icon={faCheck} title={props.title} />
) : props.inactive ? (
<PauseIcon />
) : props.empty ? (
<FontAwesomeIcon icon={faBan} title={props.title} />
) : props.warning ? (
<FontAwesomeIcon icon={faExclamationTriangle} title={props.title} />
) : (
<FontAwesomeIcon icon={faTimes} title={props.title} />
)}
{props.value && <Value>{props.value}</Value>}
</Badge>
);
const StatusIcon: React.FC<Props> = ({ title, status, ...props }) => {
return (
<Badge {...props} status={status}>
{status === "inactive" ? (
<PauseIcon title={title} />
) : (
<FontAwesomeIcon icon={(status && _iconByStatus[status]) || faTimes} title={title} />
)}
{props.value && <Value>{props.value}</Value>}
</Badge>
);
};

export default StatusIcon;
8 changes: 0 additions & 8 deletions airbyte-webapp/src/components/StatusIcon/components/Pause.tsx

This file was deleted.

12 changes: 8 additions & 4 deletions airbyte-webapp/src/components/StepsMenu/components/Step.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from "react";
import React, { useMemo } from "react";
import styled from "styled-components";

import StatusIcon from "components/StatusIcon";
import { StatusIconStatus } from "components/StatusIcon/StatusIcon";

import Status from "core/statuses";

Expand Down Expand Up @@ -63,6 +64,11 @@ const Step: React.FC<IProps> = ({ name, id, isActive, onClick, num, lightMode, s
}
};

const statusIconStatus: StatusIconStatus | undefined = useMemo(
() => (status !== Status.FAILED && !isPartialSuccess ? "success" : isPartialSuccess ? "warning" : undefined),
[status, isPartialSuccess]
);

return (
<StepView
data-id={`${id.toLowerCase()}-step`}
Expand All @@ -72,9 +78,7 @@ const Step: React.FC<IProps> = ({ name, id, isActive, onClick, num, lightMode, s
lightMode={lightMode}
>
{lightMode ? null : <Num isActive={isActive}>{num}</Num>}
{status ? (
<StatusIcon success={status !== Status.FAILED && !isPartialSuccess} warning={isPartialSuccess} />
) : null}
{status ? <StatusIcon status={statusIconStatus} /> : null}
{name}
</StepView>
);
Expand Down
Loading

0 comments on commit 6e075d1

Please sign in to comment.