Skip to content

Commit 2c9d47a

Browse files
committed
frontend: refactor devices page.
Refactor it into multiple smaller files to increase readability.
1 parent 45eab24 commit 2c9d47a

File tree

8 files changed

+731
-434
lines changed

8 files changed

+731
-434
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { useTranslation } from "react-i18next";
2+
import { Button, Modal } from "react-bootstrap";
3+
import { StateDevice } from "./types";
4+
5+
interface DeleteDeviceModalProps {
6+
show: boolean;
7+
device: StateDevice;
8+
onConfirm: () => Promise<void>;
9+
onCancel: () => void;
10+
}
11+
12+
export function DeleteDeviceModal({ show, device, onConfirm, onCancel }: DeleteDeviceModalProps) {
13+
const { t } = useTranslation("", { useSuspense: false, keyPrefix: "chargers" });
14+
15+
return (
16+
<Modal show={show} centered onHide={onCancel}>
17+
<Modal.Header>
18+
{t("delete_modal_heading", { name: device.name })}
19+
</Modal.Header>
20+
<Modal.Body>
21+
{t("delete_modal_body", { name: device.name })}
22+
</Modal.Body>
23+
<Modal.Footer>
24+
<Button
25+
variant="danger"
26+
onClick={async () => {
27+
await onConfirm();
28+
}}
29+
>
30+
{t("remove")}
31+
</Button>
32+
<Button
33+
variant="secondary"
34+
onClick={onCancel}
35+
>
36+
{t("close")}
37+
</Button>
38+
</Modal.Footer>
39+
</Modal>
40+
);
41+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { useState } from "preact/hooks";
2+
import { useTranslation } from "react-i18next";
3+
import { Button, Card, Col, Collapse, Row } from "react-bootstrap";
4+
import { Edit, Monitor, Trash2 } from "react-feather";
5+
import * as Base58 from "base58";
6+
import { Circle } from "../Circle";
7+
import { StateDevice } from "./types";
8+
9+
interface DeviceCardProps {
10+
device: StateDevice;
11+
index: number;
12+
onConnect: (device: StateDevice) => Promise<void>;
13+
onDelete: (device: StateDevice) => void;
14+
onEditNote: (device: StateDevice, index: number) => void;
15+
connectionPossible: (device: StateDevice) => boolean;
16+
formatLastStateChange: (t: (key: string, options?: any) => string, timestamp?: number | null) => string;
17+
}
18+
19+
export function DeviceCard({
20+
device,
21+
index,
22+
onConnect,
23+
onDelete,
24+
onEditNote,
25+
connectionPossible,
26+
formatLastStateChange
27+
}: DeviceCardProps) {
28+
const { t } = useTranslation("", { useSuspense: false, keyPrefix: "chargers" });
29+
const [expand, setExpand] = useState(false);
30+
31+
const trimmed_note = device.note.trim();
32+
const split = trimmed_note.split("\n");
33+
34+
return (
35+
<Card className="my-2">
36+
<Card.Header
37+
onClick={async () => {
38+
if (!connectionPossible(device)) {
39+
return;
40+
}
41+
await onConnect(device);
42+
}}
43+
className="d-flex justify-content-between align-items-center p-2d5"
44+
>
45+
<Col xs="auto" className="d-flex">
46+
{device.status === "Disconnected" ? <Circle color="danger"/> : <Circle color="success"/>}
47+
</Col>
48+
<Col className="mx-3">
49+
<h5 class="text-break" style="margin-bottom: 0;">{device.name}</h5>
50+
</Col>
51+
<Col className="d-flex justify-content-end">
52+
<Button
53+
className="me-2"
54+
variant="primary"
55+
disabled={!connectionPossible(device)}
56+
onClick={async () => {
57+
await onConnect(device);
58+
}}
59+
>
60+
<Monitor/>
61+
</Button>
62+
<Button
63+
variant="danger"
64+
onClick={async (e) => {
65+
e.stopPropagation();
66+
onDelete(device);
67+
}}
68+
>
69+
<Trash2/>
70+
</Button>
71+
</Col>
72+
</Card.Header>
73+
<Card.Body>
74+
<Row>
75+
<Col xs="auto"><b>{t("mobile_charger_id")}</b></Col>
76+
<Col className="text-end">{Base58.int_to_base58(device.uid)}</Col>
77+
</Row>
78+
<hr style="margin-top: 5px;margin-bottom: 5px;"/>
79+
<Row>
80+
<Col xs="auto"><b>{t("last_state_change")}</b></Col>
81+
<Col className="text-end">{formatLastStateChange(t, device.last_state_change)}</Col>
82+
</Row>
83+
<hr style="margin-top: 5px;margin-bottom: 5px;"/>
84+
<Row>
85+
<Col xs="auto">
86+
<Row>
87+
<b>{t("note")}</b>
88+
</Row>
89+
<Row>
90+
<Col className="p-0">
91+
<Button
92+
style="background-color:transparent;border:none;"
93+
onClick={() => {
94+
onEditNote(device, index);
95+
}}
96+
>
97+
<Edit color="#333"/>
98+
</Button>
99+
</Col>
100+
</Row>
101+
</Col>
102+
<Col
103+
onClick={split.length <= 3 ? undefined : () => setExpand(!expand)}
104+
style={{cursor: split.length <= 3 ? undefined : "pointer", whiteSpace: "pre-line", overflowWrap: "anywhere"}}
105+
>
106+
<Row>
107+
<Col className="d-flex justify-content-end" style={{textAlign: "right"}}>
108+
<div>
109+
{split.slice(0, split.length <= 3 ? 3 : 2).join("\n")}
110+
</div>
111+
</Col>
112+
</Row>
113+
<Row>
114+
<Col className="d-flex justify-content-end" style={{textAlign: "right"}}>
115+
<Collapse in={expand}>
116+
<div>
117+
{split.slice(2).join("\n")}
118+
</div>
119+
</Collapse>
120+
</Col>
121+
</Row>
122+
<Row hidden={split.length <= 3}>
123+
<Col className="d-flex justify-content-end">
124+
<a style={{fontSize: "14px", color: "blue", textDecoration: "underline"}}>
125+
{expand ? t("show_less") : t("show_more")}
126+
</a>
127+
</Col>
128+
</Row>
129+
</Col>
130+
</Row>
131+
<p style="color:red;" hidden={device.valid}>{t("no_keys")}</p>
132+
</Card.Body>
133+
</Card>
134+
);
135+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { useTranslation } from "react-i18next";
2+
import { ButtonGroup, Col, Container, Dropdown, DropdownButton } from "react-bootstrap";
3+
import Median from "median-js-bridge";
4+
import i18n from "../../i18n";
5+
import { StateDevice, SortColumn } from "./types";
6+
import { DeviceCard } from "./DeviceCard";
7+
8+
interface DeviceMobileViewProps {
9+
devices: StateDevice[];
10+
sortColumn: SortColumn;
11+
sortSequence: "asc" | "desc";
12+
onMobileSort: (column: SortColumn) => void;
13+
onSortSequenceChange: (sequence: "asc" | "desc") => void;
14+
onConnect: (device: StateDevice) => Promise<void>;
15+
onDelete: (device: StateDevice) => void;
16+
onEditNote: (device: StateDevice, index: number) => void;
17+
connectionPossible: (device: StateDevice) => boolean;
18+
formatLastStateChange: (t: (key: string, options?: any) => string, timestamp?: number | null) => string;
19+
}
20+
21+
export function DeviceMobileView({
22+
devices,
23+
sortColumn,
24+
sortSequence,
25+
onMobileSort,
26+
onSortSequenceChange,
27+
onConnect,
28+
onDelete,
29+
onEditNote,
30+
connectionPossible,
31+
formatLastStateChange
32+
}: DeviceMobileViewProps) {
33+
const { t } = useTranslation("", { useSuspense: false, keyPrefix: "chargers" });
34+
35+
const getMobileSortName = () => {
36+
switch (sortColumn) {
37+
case "name":
38+
return i18n.t("chargers.charger_name");
39+
case "status":
40+
return i18n.t("chargers.status");
41+
case "uid":
42+
return i18n.t("chargers.charger_id");
43+
case "note":
44+
return i18n.t("chargers.note");
45+
case "last_state_change":
46+
return i18n.t("chargers.last_state_change");
47+
default:
48+
return i18n.t("chargers.select_sorting");
49+
}
50+
};
51+
52+
return (
53+
<Container fluid className="d-md-none">
54+
<Col className={Median.isNativeApp() ? "mt-2" : undefined}>
55+
<ButtonGroup>
56+
<DropdownButton className="dropdown-btn" title={getMobileSortName()}>
57+
<Dropdown.Item onClick={() => onMobileSort("name")}>{t("charger_name")}</Dropdown.Item>
58+
<Dropdown.Item onClick={() => onMobileSort("uid")}>{t("charger_id")}</Dropdown.Item>
59+
<Dropdown.Item onClick={() => onMobileSort("status")}>{t("status")}</Dropdown.Item>
60+
<Dropdown.Item onClick={() => onMobileSort("last_state_change")}>{t("last_state_change")}</Dropdown.Item>
61+
<Dropdown.Item onClick={() => onMobileSort("note")}>{t("note")}</Dropdown.Item>
62+
</DropdownButton>
63+
<DropdownButton className="dropdown-btn" title={sortSequence === "asc" ? t("sorting_sequence_asc") : t("sorting_sequence_desc")}>
64+
<Dropdown.Item onClick={() => onSortSequenceChange("asc")}>{t("sorting_sequence_asc")}</Dropdown.Item>
65+
<Dropdown.Item onClick={() => onSortSequenceChange("desc")}>{t("sorting_sequence_desc")}</Dropdown.Item>
66+
</DropdownButton>
67+
</ButtonGroup>
68+
</Col>
69+
{devices.map((device, index) => (
70+
<DeviceCard
71+
key={device.id}
72+
device={device}
73+
index={index}
74+
onConnect={onConnect}
75+
onDelete={onDelete}
76+
onEditNote={onEditNote}
77+
connectionPossible={connectionPossible}
78+
formatLastStateChange={formatLastStateChange}
79+
/>
80+
))}
81+
</Container>
82+
);
83+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { useTranslation } from "react-i18next";
2+
import { Col, Row, Table } from "react-bootstrap";
3+
import { ChevronDown, ChevronUp } from "react-feather";
4+
import { StateDevice, SortColumn } from "./types";
5+
import { DeviceTableRow } from "./DeviceTableRow";
6+
7+
interface DeviceTableProps {
8+
devices: StateDevice[];
9+
sortColumn: SortColumn;
10+
sortSequence: "asc" | "desc";
11+
onSort: (column: SortColumn) => void;
12+
onConnect: (device: StateDevice) => Promise<void>;
13+
onDelete: (device: StateDevice) => void;
14+
onEditNote: (device: StateDevice, index: number) => void;
15+
connectionPossible: (device: StateDevice) => boolean;
16+
formatLastStateChange: (t: (key: string, options?: any) => string, timestamp?: number | null) => string;
17+
}
18+
19+
export function DeviceTable({
20+
devices,
21+
sortColumn,
22+
sortSequence,
23+
onSort,
24+
onConnect,
25+
onDelete,
26+
onEditNote,
27+
connectionPossible,
28+
formatLastStateChange
29+
}: DeviceTableProps) {
30+
const { t } = useTranslation("", { useSuspense: false, keyPrefix: "chargers" });
31+
32+
const getIcon = (column: SortColumn) => {
33+
if (sortColumn !== column) {
34+
// Updown Icon
35+
return <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-down"><polyline points="7 14 12 19 17 14"></polyline><polyline points="7 10 12 5 17 10"></polyline></svg>;
36+
} else if (sortSequence === "asc") {
37+
return <ChevronDown/>;
38+
} else {
39+
return <ChevronUp/>;
40+
}
41+
};
42+
43+
return (
44+
<Col className="d-none d-md-block">
45+
<Table striped hover responsive>
46+
<thead>
47+
<tr class="charger-head">
48+
<th onClick={() => onSort("status")}>
49+
<Row className="m-0">
50+
<Col className="align-content-end text-end">
51+
{getIcon("status")}
52+
</Col>
53+
</Row>
54+
</th>
55+
<th onClick={() => onSort("name")}>
56+
<Row className="flex-nowrap m-0">
57+
<Col>
58+
{t("charger_name")}
59+
</Col>
60+
<Col xs="auto">
61+
{getIcon("name")}
62+
</Col>
63+
</Row>
64+
</th>
65+
<th onClick={() => onSort("uid")}>
66+
<Row className="flex-nowrap m-0">
67+
<Col>
68+
{t("charger_id")}
69+
</Col>
70+
<Col xs="auto">
71+
{getIcon("uid")}
72+
</Col>
73+
</Row>
74+
</th>
75+
<th/>
76+
<th onClick={() => onSort("last_state_change")}>
77+
<Row className="flex-nowrap m-0">
78+
<Col>
79+
{t("last_state_change")}
80+
</Col>
81+
<Col xs="auto">
82+
{getIcon("last_state_change")}
83+
</Col>
84+
</Row>
85+
</th>
86+
<th onClick={() => onSort("note")}>
87+
<Row className="flex-nowrap m-0">
88+
<Col>
89+
{t("note")}
90+
</Col>
91+
<Col xs="auto">
92+
{getIcon("note")}
93+
</Col>
94+
</Row>
95+
</th>
96+
</tr>
97+
</thead>
98+
<tbody>
99+
{devices.map((device, index) => (
100+
<DeviceTableRow
101+
key={device.id}
102+
device={device}
103+
index={index}
104+
onConnect={onConnect}
105+
onDelete={onDelete}
106+
onEditNote={onEditNote}
107+
connectionPossible={connectionPossible}
108+
formatLastStateChange={formatLastStateChange}
109+
/>
110+
))}
111+
</tbody>
112+
</Table>
113+
</Col>
114+
);
115+
}

0 commit comments

Comments
 (0)