diff --git a/src/device/DeviceTags.tsx b/src/device/DeviceTags.tsx index 7806a59..ab1d8a5 100644 --- a/src/device/DeviceTags.tsx +++ b/src/device/DeviceTags.tsx @@ -8,7 +8,6 @@ import { ListItem, ListItemIcon, ListItemText, - Theme, Typography } from "@mui/material"; import { makeStyles } from "@mui/styles"; @@ -19,10 +18,10 @@ import TagSettings from "common/TagSettings"; import { Device, Tag } from "models"; import { filterByTag } from "pbHelpers/Tag"; import { useDeviceAPI, useSnackbar, useTagAPI } from "providers"; -import React, { useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { pond } from "protobuf-ts/pond"; -const useStyles = makeStyles((_theme: Theme) => { +const useStyles = makeStyles(() => { return ({ addIcon: { color: "var(--status-ok)" @@ -45,19 +44,19 @@ function AddDeviceTag(props: AddDeviceTagProps) { const [searchValue, setSearchValue] = useState(""); const [tags, setTags] = useState([]) - const loadTags = () => { + const loadTags = useCallback(() => { tagAPI.listTags().then(resp => { - let newTags: Tag[] = []; + const newTags: Tag[] = []; resp.data.tags.forEach((tag: pond.Tag) => { newTags.push(Tag.create(tag)) }) setTags(newTags) }) - } + }, [tagAPI]) useEffect(() => { loadTags() - }, []) + }, [loadTags]) const tagItems = tags .filter(tag => !deviceTags.some(tagSettings => tagSettings.key === tag.settings.key)) @@ -144,10 +143,10 @@ export default function DeviceTags(props: DeviceTagsProps) { const [deviceTags, setDeviceTags] = useState(device.status.tags); const previousDeviceRef = useRef(device); - const loadTags = () => { + const loadTags = useCallback(() => { // setLoading(true) tagAPI.listTags().then(resp => { - let newTags: Tag[] = []; + const newTags: Tag[] = []; resp.data.tags.forEach((tag: pond.Tag) => { newTags.push(Tag.create(tag)) }) @@ -155,7 +154,7 @@ export default function DeviceTags(props: DeviceTagsProps) { }).finally(() => { // setLoading(false) }) - } + }, [tagAPI]) useEffect(() => { if (previousDeviceRef.current !== device) { @@ -166,14 +165,16 @@ export default function DeviceTags(props: DeviceTagsProps) { useEffect(() => { loadTags() - }, []) + }, [loadTags]) const addTag = (tag: Tag) => { if (!deviceTags.some(dt => dt.key === tag.settings.key)) { deviceAPI .tag(device.id(), tag.settings.key) .then(() => { - setDeviceTags([...deviceTags, tag.settings]); + setDeviceTags(current => + current.some(dt => dt.key === tag.settings.key) ? current : [...current, tag.settings] + ); }) .catch(() => error("Failed to tag device as " + tag.name)); } @@ -184,7 +185,7 @@ export default function DeviceTags(props: DeviceTagsProps) { deviceAPI .untag(device.id(), tag.key()) .then(() => { - setDeviceTags(deviceTags.filter(t => tag.key() !== t.key)); + setDeviceTags(current => current.filter(t => tag.key() !== t.key)); }) .catch(() => error("Failed to remove tag " + tag.name + " from device")); } @@ -197,7 +198,7 @@ export default function DeviceTags(props: DeviceTagsProps) { return ( {deviceTags?.map(tagSettings => { - let pondTag = pond.Tag.create({ settings: tagSettings }) + const pondTag = pond.Tag.create({ settings: tagSettings }) return ( diff --git a/src/pages/PageContainer.tsx b/src/pages/PageContainer.tsx index 3f0e605..5ff0a9d 100644 --- a/src/pages/PageContainer.tsx +++ b/src/pages/PageContainer.tsx @@ -1,4 +1,4 @@ -import { Container, SxProps, Theme } from "@mui/material"; +import { Container, SxProps, Theme, Typography } from "@mui/material"; import { makeStyles } from "@mui/styles"; import classNames from "classnames"; // import { useMobile } from "hooks"; @@ -47,6 +47,23 @@ const useStyles = makeStyles((theme: Theme) => ({ flexDirection: "column", justifyContent: "center", alignItems: "center" + }, + buildInfo: { + position: "fixed" as const, + bottom: 56, + left: 8, + fontSize: 10, + lineHeight: 1.3, + opacity: 0.35, + color: theme.palette.text.secondary, + pointerEvents: "none" as const, + zIndex: 1, + [theme.breakpoints.up("sm")]: { + bottom: 8, + }, + [theme.breakpoints.up("md")]: { + left: `calc(${theme.spacing(9)} + 8px)`, + } } })) @@ -78,8 +95,13 @@ export const PageContainer: React.FunctionComponent = (props: Props) => { }} disableGutters maxWidth={false} - children={<>{children}} - /> + > + {children} + + {new Date(__BUILD_DATE__).toLocaleDateString()}
+ {__GIT_HASH__} +
+ ); }; diff --git a/src/providers/pond/deviceAPI.tsx b/src/providers/pond/deviceAPI.tsx index a7c0352..8b778d5 100644 --- a/src/providers/pond/deviceAPI.tsx +++ b/src/providers/pond/deviceAPI.tsx @@ -700,8 +700,12 @@ export default function DeviceProvider(props: PropsWithChildren) { }; const setTags = (id: number, tags: string[]) => { + const keys = getContextKeys() + const types = getContextTypes() + let url = "/devices/" + id + "/tags" + "?keys=" + keys + "&types=" + types + if (as && !keys.includes(as)) url += "&as=" + as return new Promise((resolve, reject) => { - put(pondURL("/devices/" + id + "/tags"), { tags }).then(resp => { + put(pondURL(url), { tags }).then(resp => { return resolve(resp) }).catch(err => { return reject(err) @@ -746,8 +750,12 @@ export default function DeviceProvider(props: PropsWithChildren) { }; const untag = (id: number, tag: string) => { + const keys = getContextKeys() + const types = getContextTypes() + let url = "/devices/" + id + "/tags/" + tag + "?keys=" + keys + "&types=" + types + if (as && !keys.includes(as)) url += "&as=" + as return new Promise((resolve, reject) => { - del(pondURL("/devices/" + id + "/tags/" + tag)).then(resp => { + del(pondURL(url)).then(resp => { return resolve(resp) }).catch(err => { return reject(err) diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 11f02fe..db728ea 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1 +1,4 @@ /// + +declare const __BUILD_DATE__: string +declare const __GIT_HASH__: string diff --git a/vite.config.ts b/vite.config.ts index ed1b6f1..7ff8324 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,6 +5,7 @@ import { VitePWA } from 'vite-plugin-pwa'; import * as path from 'path' // ✅ Import path module import { readFileSync, renameSync, existsSync, unlinkSync } from 'node:fs' import { fileURLToPath } from 'node:url' +import { execSync } from 'node:child_process' const rootDir = path.dirname(fileURLToPath(import.meta.url)) @@ -49,7 +50,19 @@ function emitLocalnetShellAsIndexHtml (mode: string): Plugin { // https://vitejs.dev/config/ export default defineConfig(({ command, mode }): UserConfig => { const useLocalnetShell = mode === LOCALNET_MODE + + let gitHash = 'unknown' + try { + gitHash = execSync('git rev-parse --short HEAD').toString().trim() + } catch {} + + const buildDate = new Date().toISOString() + return { + define: { + __BUILD_DATE__: JSON.stringify(buildDate), + __GIT_HASH__: JSON.stringify(gitHash), + }, plugins: [ useLocalnetShellHtml(mode, command), emitLocalnetShellAsIndexHtml(mode),