diff --git a/packages/webapp/public/locales/en/translation.json b/packages/webapp/public/locales/en/translation.json index 4fa8609e16..91619c2a0c 100644 --- a/packages/webapp/public/locales/en/translation.json +++ b/packages/webapp/public/locales/en/translation.json @@ -1897,7 +1897,13 @@ "STATUS": { "ACTIVE": "Active", "INACTIVE": "Inactive", - "INVITED": "Invited" + "INVITED": "Invited", + "OFFLINE": "Offline", + "ONLINE": "Online", + "SENSOR": { + "OFFLINE_TOOLTIP": "Device has not sent data for more than 12 hours", + "ONLINE_TOOLTIP": "Device has sent data within the last 12 hours" + } }, "SURVEY_STACK": { "PRODUCED": "Produced on", diff --git a/packages/webapp/src/assets/images/status-indicator-dot.svg b/packages/webapp/src/assets/images/status-indicator-dot.svg new file mode 100644 index 0000000000..480ad18257 --- /dev/null +++ b/packages/webapp/src/assets/images/status-indicator-dot.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/webapp/src/components/HoverPill/index.tsx b/packages/webapp/src/components/HoverPillOverflowList/index.tsx similarity index 54% rename from packages/webapp/src/components/HoverPill/index.tsx rename to packages/webapp/src/components/HoverPillOverflowList/index.tsx index 1b2c55cb2d..b41449fd4e 100644 --- a/packages/webapp/src/components/HoverPill/index.tsx +++ b/packages/webapp/src/components/HoverPillOverflowList/index.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2024 LiteFarm.org + * Copyright 2024, 2025 LiteFarm.org * This file is part of LiteFarm. * * LiteFarm is free software: you can redistribute it and/or modify @@ -18,16 +18,21 @@ import { Tooltip } from '@mui/material'; import styles from './styles.module.scss'; import { Semibold, Main } from '../Typography'; -export interface HoverPillProps { +export interface HoverPillOverflowListProps { items: string[]; + noneText?: string; } -export const HoverPill = ({ items }: HoverPillProps) => { +export const HoverPillOverflowList = ({ items, noneText = '' }: HoverPillOverflowListProps) => { const { t } = useTranslation(); + if (items.length === 0) { + return
{noneText}
; + } + const hoverContent = ( <> - {items.map((item, index) => ( + {items.slice(1).map((item, index) => (
{item}
@@ -48,21 +53,26 @@ export const HoverPill = ({ items }: HoverPillProps) => { }; return ( - -
- - {t('HOVERPILL.PLUS_OTHERS_COUNT', { count: items.length })} - -
-
+
+
{items[0]}
+ {items.length > 1 && ( + +
+ + {t('HOVERPILL.PLUS_OTHERS_COUNT', { count: items.length - 1 })} + +
+
+ )} +
); }; diff --git a/packages/webapp/src/components/HoverPill/styles.module.scss b/packages/webapp/src/components/HoverPillOverflowList/styles.module.scss similarity index 90% rename from packages/webapp/src/components/HoverPill/styles.module.scss rename to packages/webapp/src/components/HoverPillOverflowList/styles.module.scss index 6e461b7cff..f0db9c8859 100644 --- a/packages/webapp/src/components/HoverPill/styles.module.scss +++ b/packages/webapp/src/components/HoverPillOverflowList/styles.module.scss @@ -1,5 +1,5 @@ /* - * Copyright 2024 LiteFarm.org + * Copyright 2024, 2025 LiteFarm.org * This file is part of LiteFarm. * * LiteFarm is free software: you can redistribute it and/or modify @@ -13,6 +13,17 @@ * GNU General Public License for more details, see . */ +.container { + display: flex; + gap: 8px; +} + +.itemText { + color: var(--Colors-Neutral-Neutral-700); + font-size: 16px; + font-weight: 400; +} + .pillText, .detailText { user-select: none; diff --git a/packages/webapp/src/components/StatusIndicatorPill/index.tsx b/packages/webapp/src/components/StatusIndicatorPill/index.tsx new file mode 100644 index 0000000000..a0a2a4f3a0 --- /dev/null +++ b/packages/webapp/src/components/StatusIndicatorPill/index.tsx @@ -0,0 +1,88 @@ +/* + * Copyright 2025 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +import clsx from 'clsx'; +import { useTranslation } from 'react-i18next'; +import { Tooltip } from '@mui/material'; +import { ReactComponent as StatusIndicatorDot } from '../../assets/images/status-indicator-dot.svg'; +import styles from './styles.module.scss'; +import { Semibold, Main } from '../Typography'; + +export enum Status { + ONLINE = 'ONLINE', + OFFLINE = 'OFFLINE', +} + +export interface StatusIndicatorPillProps { + status: Status; + pillText: string; + tooltipText?: string; + showHoverTooltip?: boolean; +} + +export const StatusIndicatorPill = ({ + status, + pillText, + tooltipText = '', + showHoverTooltip = true, +}: StatusIndicatorPillProps) => { + const { t } = useTranslation(); + + const isOnline = status === Status.ONLINE; + + const hoverContent =
{t(tooltipText)}
; + + // https://mui.com/material-ui/react-tooltip/#distance-from-anchor + const PopperProps = { + modifiers: [ + { + name: 'offset', + options: { + offset: [0, 4], + }, + }, + ], + }; + + const statusPill = ( +
+ + {t(pillText)} +
+ ); + + return showHoverTooltip ? ( + + {statusPill} + + ) : ( + statusPill + ); +}; diff --git a/packages/webapp/src/components/StatusIndicatorPill/styles.module.scss b/packages/webapp/src/components/StatusIndicatorPill/styles.module.scss new file mode 100644 index 0000000000..52a330ada3 --- /dev/null +++ b/packages/webapp/src/components/StatusIndicatorPill/styles.module.scss @@ -0,0 +1,97 @@ +/* + * Copyright 2025 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +.pill { + display: inline-flex; + align-items: center; + gap: 4px; + + padding: 2px 4px; + border-radius: 40px; + + &.hover { + cursor: default; + } +} + +.pill.online { + background: var(--Colors-Secondary-Secondary-green-100); + + .pillText { + color: var(--Colors-Secondary-Secondary-green-700); + } + + &.hover:hover { + background: var(--Colors-Secondary-Secondary-green-700); + + .pillText { + color: var(--Colors-Secondary-Secondary-green-50); + } + + svg circle { + fill: var(--Colors-Secondary-Secondary-green-50); + } + } +} + +.pill.offline { + background: var(--Colors-Accent---singles-Red-light); + + svg circle { + fill: var(--Colors-Accent---singles-Red-full); + } + + .pillText { + color: var(--Colors-Accent---singles-Red-full); + } + + &.hover:hover { + background: var(--Colors-Accent---singles-Red-full); + + .pillText { + color: var(--Colors-Accent---singles-Red-light); + } + + svg circle { + fill: var(--Colors-Accent---singles-Red-light); + } + } +} + +.pillText, +.hoverText { + font-family: 'Open Sans'; + user-select: none; + font-weight: 400; +} + +.pillText { + font-size: 16px; + line-height: 20px; +} + +.hoverText { + font-size: 14px; + line-height: normal; + color: #000; +} + +.tooltipContainer { + margin: 0 !important; // override MUI's default margin + padding: 8px 16px; + border-radius: 8px; + background: var(--white, #fff); + box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.25); +} diff --git a/packages/webapp/src/components/Table/Cell/CellTypes/HoverPillOverflow.tsx b/packages/webapp/src/components/Table/Cell/CellTypes/HoverPillOverflow.tsx deleted file mode 100644 index b956844ceb..0000000000 --- a/packages/webapp/src/components/Table/Cell/CellTypes/HoverPillOverflow.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2024 LiteFarm.org - * This file is part of LiteFarm. - * - * LiteFarm is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LiteFarm is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details, see . - */ -import clsx from 'clsx'; -import { HoverPill, HoverPillProps } from '../../../HoverPill'; -import styles from '../styles.module.scss'; - -export type HoverPillOverflowProps = HoverPillProps & { noneText?: string }; - -const HoverPillOverFlow = ({ items, noneText = '' }: HoverPillOverflowProps) => { - return ( -
- {items.length === 0 ? ( - {noneText} - ) : ( - {items[0]} - )} - {items.length > 1 && } -
- ); -}; - -export default HoverPillOverFlow; diff --git a/packages/webapp/src/components/Table/Cell/index.tsx b/packages/webapp/src/components/Table/Cell/index.tsx index 03b4be04d0..818cbe51e2 100644 --- a/packages/webapp/src/components/Table/Cell/index.tsx +++ b/packages/webapp/src/components/Table/Cell/index.tsx @@ -13,27 +13,31 @@ * GNU General Public License for more details, see . */ import Plain from './CellTypes/Plain'; -import HoverPillOverflow from './CellTypes/HoverPillOverflow'; import RightChevronLink from './CellTypes/RightChevronLink'; import IconText from './CellTypes/IconText'; +import { StatusIndicatorPill, StatusIndicatorPillProps } from '../../StatusIndicatorPill'; +import { HoverPillOverflowList, HoverPillOverflowListProps } from '../../HoverPillOverflowList'; import { CellKind } from '../types'; -import type { HoverPillOverflowProps } from './CellTypes/HoverPillOverflow'; import type { IconTextProps } from './CellTypes/IconText'; import type { PlainCellProps } from './CellTypes/Plain'; import type { RightChevronLinkProps } from './CellTypes/RightChevronLink'; -type HoverPillOverflowPropsStrategy = HoverPillOverflowProps & { +type HoverPillOverflowPropsStrategy = HoverPillOverflowListProps & { kind: CellKind.HOVER_PILL_OVERFLOW; }; type IconTextPropsStrategy = IconTextProps & { kind: CellKind.ICON_TEXT }; type PlainCellPropsStrategy = PlainCellProps & { kind: CellKind.PLAIN }; type RightChevronLinkPropsStrategy = RightChevronLinkProps & { kind: CellKind.RIGHT_CHEVRON_LINK }; +type StatusIndicatorPillPropsStrategy = StatusIndicatorPillProps & { + kind: CellKind.STATUS_INDICATOR_PILL; +}; type CellStrategyProps = | HoverPillOverflowPropsStrategy | IconTextPropsStrategy | PlainCellPropsStrategy - | RightChevronLinkPropsStrategy; + | RightChevronLinkPropsStrategy + | StatusIndicatorPillPropsStrategy; /** * A component that selects between available Cell styles. @@ -42,13 +46,15 @@ type CellStrategyProps = const Cell = ({ kind, ...props }: CellStrategyProps) => { switch (kind) { case CellKind.HOVER_PILL_OVERFLOW: - return ; + return ; case CellKind.ICON_TEXT: return ; case CellKind.PLAIN: return ; case CellKind.RIGHT_CHEVRON_LINK: return ; + case CellKind.STATUS_INDICATOR_PILL: + return ; default: const _exhaustiveCheck: never = kind; return null; diff --git a/packages/webapp/src/components/Table/Cell/styles.module.scss b/packages/webapp/src/components/Table/Cell/styles.module.scss index 65c9c88a24..2dadeb6d41 100644 --- a/packages/webapp/src/components/Table/Cell/styles.module.scss +++ b/packages/webapp/src/components/Table/Cell/styles.module.scss @@ -73,10 +73,6 @@ font-size: 12px; } -.marginRight8px { - margin-right: 8px; -} - .highlightedText { display: flex; padding: 4px; diff --git a/packages/webapp/src/components/Table/types.ts b/packages/webapp/src/components/Table/types.ts index e2a5a3d5c6..618e899882 100644 --- a/packages/webapp/src/components/Table/types.ts +++ b/packages/webapp/src/components/Table/types.ts @@ -29,6 +29,7 @@ export enum CellKind { ICON_TEXT = 'iconText', PLAIN = 'plain', RIGHT_CHEVRON_LINK = 'rightChevronLink', + STATUS_INDICATOR_PILL = 'StatusIndicatorPill', } export enum Alignment { diff --git a/packages/webapp/src/stories/HoverPill/HoverPill.stories.tsx b/packages/webapp/src/stories/HoverPillOverflowList/HoverPillOverflowList.stories.tsx similarity index 75% rename from packages/webapp/src/stories/HoverPill/HoverPill.stories.tsx rename to packages/webapp/src/stories/HoverPillOverflowList/HoverPillOverflowList.stories.tsx index a81bbf2153..b290225089 100644 --- a/packages/webapp/src/stories/HoverPill/HoverPill.stories.tsx +++ b/packages/webapp/src/stories/HoverPillOverflowList/HoverPillOverflowList.stories.tsx @@ -17,17 +17,20 @@ import { ReactNode } from 'react'; import { Meta, StoryObj } from '@storybook/react'; import clsx from 'clsx'; import { componentDecoratorsFullHeight } from '../Pages/config/Decorators'; -import { HoverPill, HoverPillProps } from '../../components/HoverPill'; +import { + HoverPillOverflowList, + HoverPillOverflowListProps, +} from '../../components/HoverPillOverflowList'; import styles from './styles.module.scss'; -type HoverPillStoryProps = HoverPillProps & { +type HoverPillStoryProps = HoverPillOverflowListProps & { position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center'; }; // https://storybook.js.org/docs/writing-stories/typescript const meta: Meta = { - title: 'Components/HoverPill', - component: HoverPill, + title: 'Components/HoverPillOverflowList', + component: HoverPillOverflowList, argTypes: { position: { control: 'select', @@ -66,16 +69,35 @@ const Wrapper = ({ children, position = 'center' }: WrapperProps) => { return
{children}
; }; -type Story = StoryObj; +type Story = StoryObj; -export const Plural: Story = { +export const NoItems: Story = { args: { - items: ['Heifers', 'Foot Rot treatment', 'Feeding change'], + items: [], + }, +}; + +export const NoItemsWithNoneText: Story = { + args: { + items: [], + noneText: 'none', + }, +}; + +export const OneItem: Story = { + args: { + items: ['Heifers'], }, }; -export const Singular: Story = { +export const TwoItems: Story = { args: { - items: ['Feeding change'], + items: ['Heifers', 'Foot Rot treatment'], + }, +}; + +export const ThreeItems: Story = { + args: { + items: ['Heifers', 'Foot Rot treatment', 'Feeding change'], }, }; diff --git a/packages/webapp/src/stories/HoverPill/styles.module.scss b/packages/webapp/src/stories/HoverPillOverflowList/styles.module.scss similarity index 100% rename from packages/webapp/src/stories/HoverPill/styles.module.scss rename to packages/webapp/src/stories/HoverPillOverflowList/styles.module.scss diff --git a/packages/webapp/src/stories/StatusIndicatorPill/StatusIndicatorPill.stories.tsx b/packages/webapp/src/stories/StatusIndicatorPill/StatusIndicatorPill.stories.tsx new file mode 100644 index 0000000000..1784cbd47a --- /dev/null +++ b/packages/webapp/src/stories/StatusIndicatorPill/StatusIndicatorPill.stories.tsx @@ -0,0 +1,98 @@ +/* + * Copyright 2025 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +import { ReactNode } from 'react'; +import { Meta, StoryObj } from '@storybook/react'; +import clsx from 'clsx'; +import { componentDecoratorsFullHeight } from '../Pages/config/Decorators'; +import { + StatusIndicatorPill, + StatusIndicatorPillProps, + Status, +} from '../../components/StatusIndicatorPill'; +import styles from './styles.module.scss'; + +type StatusIndicatorPillStoryProps = StatusIndicatorPillProps & { + position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center'; +}; + +// https://storybook.js.org/docs/writing-stories/typescript +const meta: Meta = { + title: 'Components/StatusIndicatorPill', + component: StatusIndicatorPill, + argTypes: { + position: { + control: 'select', + options: ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'center'], + description: 'Position of the status indicator pill within the Storybook frame', + }, + }, + decorators: [ + (Story, context) => ( + + + + ), + ...componentDecoratorsFullHeight, + ], + + // To reduce the height of the docs page canvas when using 100vh decorator + // See https://github.com/storybookjs/storybook/issues/13765 + parameters: { + docs: { + story: { + inline: false, + iframeHeight: 300, + }, + }, + }, +}; +export default meta; + +interface WrapperProps { + children: ReactNode; + position?: string; +} + +const Wrapper = ({ children, position = 'center' }: WrapperProps) => { + return
{children}
; +}; + +type Story = StoryObj; + +export const Online: Story = { + args: { + status: Status.ONLINE, + pillText: 'Online', + tooltipText: 'Device has sent data in the last 12 hours', + }, +}; + +export const Offline: Story = { + args: { + status: Status.OFFLINE, + pillText: 'Offline', + tooltipText: 'Device has not sent data in the last 12 hours', + }, +}; + +export const HoverTooltipDisabled: Story = { + args: { + status: Status.OFFLINE, + pillText: 'Offline', + tooltipText: 'Device has not sent data in the last 12 hours', + showHoverTooltip: false, + }, +}; diff --git a/packages/webapp/src/stories/StatusIndicatorPill/styles.module.scss b/packages/webapp/src/stories/StatusIndicatorPill/styles.module.scss new file mode 100644 index 0000000000..fe1034487a --- /dev/null +++ b/packages/webapp/src/stories/StatusIndicatorPill/styles.module.scss @@ -0,0 +1,47 @@ +/* + * Copyright 2025 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +.wrapper { + position: relative; + height: 100%; + width: 100%; + display: flex; + padding: 24px; +} + +.center { + align-items: center; + justify-content: center; +} + +.top-left { + align-items: flex-start; + justify-content: flex-start; +} + +.top-right { + align-items: flex-start; + justify-content: flex-end; +} + +.bottom-left { + align-items: flex-end; + justify-content: flex-start; +} + +.bottom-right { + align-items: flex-end; + justify-content: flex-end; +} diff --git a/packages/webapp/src/stories/Table/TableCells.stories.jsx b/packages/webapp/src/stories/Table/TableCells.stories.jsx index 07f965d6e1..d07b0f1493 100644 --- a/packages/webapp/src/stories/Table/TableCells.stories.jsx +++ b/packages/webapp/src/stories/Table/TableCells.stories.jsx @@ -17,6 +17,7 @@ import { v2TableDecorator } from '../Pages/config/Decorators'; import Table from '../../components/Table'; import Cell from '../../components/Table/Cell'; import { TableKind, CellKind } from '../../components/Table/types'; +import { Status } from '../../components/StatusIndicatorPill'; export default { title: 'Components/Tables/Cells', @@ -42,6 +43,22 @@ const getFakeColumns = () => { label: 'Revenue', format: (d) => , }, + { + id: 'StatusIndicatorPill', + label: 'Availability', + format: (d) => { + const isAvailable = Math.random() < 3 / 4; + return ( + + ); + }, + sortable: false, + }, { id: 'rightChevronLink', label: '',