Merge branch 'local_server' into dev_environment

This commit is contained in:
Carter 2026-05-25 12:25:35 -06:00
commit 73ad9c6816
5 changed files with 66 additions and 19 deletions

View file

@ -8,7 +8,6 @@ import {
ListItem, ListItem,
ListItemIcon, ListItemIcon,
ListItemText, ListItemText,
Theme,
Typography Typography
} from "@mui/material"; } from "@mui/material";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
@ -19,10 +18,10 @@ import TagSettings from "common/TagSettings";
import { Device, Tag } from "models"; import { Device, Tag } from "models";
import { filterByTag } from "pbHelpers/Tag"; import { filterByTag } from "pbHelpers/Tag";
import { useDeviceAPI, useSnackbar, useTagAPI } from "providers"; 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"; import { pond } from "protobuf-ts/pond";
const useStyles = makeStyles((_theme: Theme) => { const useStyles = makeStyles(() => {
return ({ return ({
addIcon: { addIcon: {
color: "var(--status-ok)" color: "var(--status-ok)"
@ -45,19 +44,19 @@ function AddDeviceTag(props: AddDeviceTagProps) {
const [searchValue, setSearchValue] = useState(""); const [searchValue, setSearchValue] = useState("");
const [tags, setTags] = useState<Tag[]>([]) const [tags, setTags] = useState<Tag[]>([])
const loadTags = () => { const loadTags = useCallback(() => {
tagAPI.listTags().then(resp => { tagAPI.listTags().then(resp => {
let newTags: Tag[] = []; const newTags: Tag[] = [];
resp.data.tags.forEach((tag: pond.Tag) => { resp.data.tags.forEach((tag: pond.Tag) => {
newTags.push(Tag.create(tag)) newTags.push(Tag.create(tag))
}) })
setTags(newTags) setTags(newTags)
}) })
} }, [tagAPI])
useEffect(() => { useEffect(() => {
loadTags() loadTags()
}, []) }, [loadTags])
const tagItems = tags const tagItems = tags
.filter(tag => !deviceTags.some(tagSettings => tagSettings.key === tag.settings.key)) .filter(tag => !deviceTags.some(tagSettings => tagSettings.key === tag.settings.key))
@ -144,10 +143,10 @@ export default function DeviceTags(props: DeviceTagsProps) {
const [deviceTags, setDeviceTags] = useState<pond.TagSettings[]>(device.status.tags); const [deviceTags, setDeviceTags] = useState<pond.TagSettings[]>(device.status.tags);
const previousDeviceRef = useRef(device); const previousDeviceRef = useRef(device);
const loadTags = () => { const loadTags = useCallback(() => {
// setLoading(true) // setLoading(true)
tagAPI.listTags().then(resp => { tagAPI.listTags().then(resp => {
let newTags: Tag[] = []; const newTags: Tag[] = [];
resp.data.tags.forEach((tag: pond.Tag) => { resp.data.tags.forEach((tag: pond.Tag) => {
newTags.push(Tag.create(tag)) newTags.push(Tag.create(tag))
}) })
@ -155,7 +154,7 @@ export default function DeviceTags(props: DeviceTagsProps) {
}).finally(() => { }).finally(() => {
// setLoading(false) // setLoading(false)
}) })
} }, [tagAPI])
useEffect(() => { useEffect(() => {
if (previousDeviceRef.current !== device) { if (previousDeviceRef.current !== device) {
@ -166,14 +165,16 @@ export default function DeviceTags(props: DeviceTagsProps) {
useEffect(() => { useEffect(() => {
loadTags() loadTags()
}, []) }, [loadTags])
const addTag = (tag: Tag) => { const addTag = (tag: Tag) => {
if (!deviceTags.some(dt => dt.key === tag.settings.key)) { if (!deviceTags.some(dt => dt.key === tag.settings.key)) {
deviceAPI deviceAPI
.tag(device.id(), tag.settings.key) .tag(device.id(), tag.settings.key)
.then(() => { .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)); .catch(() => error("Failed to tag device as " + tag.name));
} }
@ -184,7 +185,7 @@ export default function DeviceTags(props: DeviceTagsProps) {
deviceAPI deviceAPI
.untag(device.id(), tag.key()) .untag(device.id(), tag.key())
.then(() => { .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")); .catch(() => error("Failed to remove tag " + tag.name + " from device"));
} }
@ -197,7 +198,7 @@ export default function DeviceTags(props: DeviceTagsProps) {
return ( return (
<React.Fragment> <React.Fragment>
{deviceTags?.map(tagSettings => { {deviceTags?.map(tagSettings => {
let pondTag = pond.Tag.create({ settings: tagSettings }) const pondTag = pond.Tag.create({ settings: tagSettings })
return ( return (
<Grid key={"device-tags-"+tagSettings.key}> <Grid key={"device-tags-"+tagSettings.key}>
<DeviceTag tag={Tag.create(pondTag)} removeTagFromDevice={removeTagFromDevice} /> <DeviceTag tag={Tag.create(pondTag)} removeTagFromDevice={removeTagFromDevice} />

View file

@ -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 { makeStyles } from "@mui/styles";
import classNames from "classnames"; import classNames from "classnames";
// import { useMobile } from "hooks"; // import { useMobile } from "hooks";
@ -47,6 +47,23 @@ const useStyles = makeStyles((theme: Theme) => ({
flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
alignItems: "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: Props) => {
}} }}
disableGutters disableGutters
maxWidth={false} maxWidth={false}
children={<>{children}</>} >
/> {children}
<Typography component="div" className={classes.buildInfo}>
{new Date(__BUILD_DATE__).toLocaleDateString()}<br />
{__GIT_HASH__}
</Typography>
</Container>
); );
}; };

View file

@ -700,8 +700,12 @@ export default function DeviceProvider(props: PropsWithChildren<Props>) {
}; };
const setTags = (id: number, tags: string[]) => { 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<AxiosResponse>((resolve, reject) => { return new Promise<AxiosResponse>((resolve, reject) => {
put(pondURL("/devices/" + id + "/tags"), { tags }).then(resp => { put(pondURL(url), { tags }).then(resp => {
return resolve(resp) return resolve(resp)
}).catch(err => { }).catch(err => {
return reject(err) return reject(err)
@ -746,8 +750,12 @@ export default function DeviceProvider(props: PropsWithChildren<Props>) {
}; };
const untag = (id: number, tag: string) => { 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<AxiosResponse>((resolve, reject) => { return new Promise<AxiosResponse>((resolve, reject) => {
del(pondURL("/devices/" + id + "/tags/" + tag)).then(resp => { del(pondURL(url)).then(resp => {
return resolve(resp) return resolve(resp)
}).catch(err => { }).catch(err => {
return reject(err) return reject(err)

3
src/vite-env.d.ts vendored
View file

@ -1 +1,4 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
declare const __BUILD_DATE__: string
declare const __GIT_HASH__: string

View file

@ -5,6 +5,7 @@ import { VitePWA } from 'vite-plugin-pwa';
import * as path from 'path' // ✅ Import path module import * as path from 'path' // ✅ Import path module
import { readFileSync, renameSync, existsSync, unlinkSync } from 'node:fs' import { readFileSync, renameSync, existsSync, unlinkSync } from 'node:fs'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { execSync } from 'node:child_process'
const rootDir = path.dirname(fileURLToPath(import.meta.url)) const rootDir = path.dirname(fileURLToPath(import.meta.url))
@ -49,7 +50,19 @@ function emitLocalnetShellAsIndexHtml (mode: string): Plugin {
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig(({ command, mode }): UserConfig => { export default defineConfig(({ command, mode }): UserConfig => {
const useLocalnetShell = mode === LOCALNET_MODE 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 { return {
define: {
__BUILD_DATE__: JSON.stringify(buildDate),
__GIT_HASH__: JSON.stringify(gitHash),
},
plugins: [ plugins: [
useLocalnetShellHtml(mode, command), useLocalnetShellHtml(mode, command),
emitLocalnetShellAsIndexHtml(mode), emitLocalnetShellAsIndexHtml(mode),