Skip to content

Commit 273afb9

Browse files
committed
Merge branch 'main' into ang/csv-parsing
2 parents 08b7566 + 4cb7d6b commit 273afb9

6 files changed

Lines changed: 212 additions & 131 deletions

File tree

.github/workflows/tauri.yml

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
name: Build and Upload Tauri App
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
build-and-upload-tauri:
10+
permissions:
11+
contents: write
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
include:
16+
# if you add new targets, remember to add a new actions/upload-artifact call at the end of this workflow
17+
- platform: 'ubuntu-22.04'
18+
args: '--bundles rpm'
19+
runs-on: ${{ matrix.platform }}
20+
steps:
21+
- name: Checkout codebase
22+
uses: actions/checkout@v6.0.2
23+
24+
- name: Setup pnpm
25+
uses: pnpm/action-setup@v4
26+
with:
27+
run_install: false
28+
# cache is documented but unavailable as of v4.2
29+
# when available, enable and remove separate npm setup below
30+
# cache: true
31+
32+
- name: Install pnpm dependencies with caching
33+
uses: actions/setup-node@v4
34+
with:
35+
node-version: 23.11.0
36+
cache: pnpm
37+
- run: pnpm install
38+
39+
- name: Install Rust stable
40+
uses: dtolnay/rust-toolchain@stable
41+
42+
- name: Install dependencies (ubuntu only)
43+
if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
44+
run: |
45+
sudo apt-get update
46+
sudo apt-get install -y libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev
47+
48+
- name: Install Tauri
49+
run: pnpm add -D @tauri-apps/cli@1
50+
51+
- name: Configure Tauri
52+
run: |
53+
pnpm tauri init \
54+
--app-name "Squirrel" \
55+
--window-title "Squirrel" \
56+
--dist-dir ../dist \
57+
--dev-path http://localhost:5173 \
58+
--before-dev-command "pnpm dev" \
59+
--before-build-command "pnpm build"
60+
61+
- name: Build Tauri package
62+
id: build
63+
uses: tauri-apps/tauri-action@v0.6.1
64+
env:
65+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
66+
with:
67+
args: >-
68+
${{ matrix.args }}
69+
--config {"tauri":{"bundle":{"identifier":"edu.stanford.slac.squirrel"}}}
70+
71+
- name: Upload Tauri artifacts
72+
uses: actions/upload-artifact@v6
73+
with:
74+
# steps.build.outputs.artifactPaths uses absolute paths, which didn't resolve properly in `path`
75+
name: squirrel-rpm
76+
path: src-tauri/target/release/bundle/rpm/*.rpm

.prettierignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@ build
1111
*.log
1212
.vscode
1313
.idea
14+
15+
# Generated files
16+
src/routeTree.gen.ts

CONTRIBUTING.rst

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,4 @@ Ready to contribute? Here's how to set up `squirrel` for local development.
9393
Pull Request Guidelines
9494
-----------------------
9595

96-
Before you submit a pull request, check that it meets these guidelines:
97-
98-
1. The pull request should include tests.
99-
2. If the pull request adds functionality, the docs should be updated. Put your
100-
new functionality into a function with a docstring, and add the feature to
101-
the list in README.rst.
102-
3. The pull request should work for Python 3.9 and up. Check the GitHub Actions status
103-
and make sure that the tests pass for all supported Python versions.
96+
1. If the pull request adds functionality, any new functions should contain docstrings, and the feature should be added to the list in README.md.

README.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
A web-based application for configuration management of EPICS Process Variables (PVs).
44

5-
Squirrel provides a responsive, high-performance interface for creating snapshots of control system states, comparing configurations, and managing PV settings. It communicates with a backend API (score-backend) that handles persistence and EPICS control system integration.
5+
Squirrel provides a responsive, high-performance interface for creating snapshots of control system states, comparing configurations, and managing PV settings. It communicates with a backend API ([react-squirrel-backend](https://github.com/slaclab/react-squirrel-backend)) that handles persistence and EPICS control system integration.
66

77
## Features
88

@@ -32,7 +32,7 @@ Squirrel provides a responsive, high-performance interface for creating snapshot
3232

3333
- Node.js 18+
3434
- pnpm
35-
- Backend API running on `http://localhost:8080`
35+
- [Backend](https://github.com/slaclab/react-squirrel-backend) listening for requests on `http://localhost:8080`
3636

3737
### Installation
3838

@@ -192,7 +192,3 @@ See [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed documentation on:
192192
- Component structure
193193
- API layer design
194194
- Performance optimizations
195-
196-
## License
197-
198-
[Add license information]

src/pages/TagPage.tsx

Lines changed: 106 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ import {
2323
ListItem,
2424
ListItemText,
2525
ListItemSecondaryAction,
26+
InputAdornment,
27+
Tooltip,
2628
} from '@mui/material';
27-
import { Add, Delete, Edit, NoteOutlined } from '@mui/icons-material';
29+
import { Add, Delete, Edit, NewReleasesOutlined, NoteOutlined } from '@mui/icons-material';
2830
import { TagGroup, Tag } from '../types';
2931

3032
interface TagPageProps {
@@ -156,11 +158,6 @@ export function TagPage({
156158
try {
157159
await onAddTag(selectedGroup.id, newTagName);
158160
setNewTagName('');
159-
// Refresh the selected group data
160-
const updatedGroup = tagGroups.find((g) => g.id === selectedGroup.id);
161-
if (updatedGroup) {
162-
setSelectedGroup(updatedGroup);
163-
}
164161
} catch (err) {
165162
// eslint-disable-next-line no-console
166163
console.error('Failed to add tag:', err);
@@ -173,7 +170,7 @@ export function TagPage({
173170
if (!selectedGroup || !onEditTag) return;
174171

175172
try {
176-
await onEditTag(selectedGroup.id, tag.name, tagName, tagDescription);
173+
await onEditTag(selectedGroup.id, tag.id, tagName, tagDescription);
177174
} catch (err) {
178175
// eslint-disable-next-line no-console
179176
console.error('Failed to edit tag:', err);
@@ -185,18 +182,10 @@ export function TagPage({
185182
const handleDeleteTag = async (tag: Tag) => {
186183
// eslint-disable-next-line no-alert, no-restricted-globals
187184
if (!confirm(`Delete tag "${tag.name}"?`)) return;
188-
189185
if (!selectedGroup || !onDeleteTag) return;
190186

191187
try {
192-
// We need to find the tag ID - for now we're using tag name
193-
// The backend API expects tag ID, so we'll need to update this
194-
await onDeleteTag(selectedGroup.id, tag.name);
195-
// Refresh the selected group data
196-
const updatedGroup = tagGroups.find((g) => g.id === selectedGroup.id);
197-
if (updatedGroup) {
198-
setSelectedGroup(updatedGroup);
199-
}
188+
await onDeleteTag(selectedGroup.id, tag.id);
200189
} catch (err) {
201190
// eslint-disable-next-line no-console
202191
console.error('Failed to delete tag:', err);
@@ -329,76 +318,114 @@ export function TagPage({
329318
<Box sx={{ px: 3, pt: 1, borderBottom: '1px solid #eee' }}>
330319
<TextField
331320
fullWidth
321+
margin="normal"
322+
disabled={!isAdmin}
332323
label="Title"
333324
value={groupName}
334325
onChange={(e) => setGroupName(e.target.value)}
335-
margin="normal"
336-
disabled={!isAdmin}
326+
InputProps={{
327+
endAdornment: (
328+
<InputAdornment position="end">
329+
{editMode && selectedGroup && groupName !== selectedGroup.name && (
330+
<Tooltip title="Group title has unsaved changes">
331+
<NewReleasesOutlined color="warning" />
332+
</Tooltip>
333+
)}
334+
</InputAdornment>
335+
),
336+
}}
337337
/>
338338
<TextField
339339
fullWidth
340-
label="Description"
341-
value={groupDescription}
342-
onChange={(e) => setGroupDescription(e.target.value)}
343340
margin="normal"
344-
disabled={!isAdmin}
345341
multiline
346342
rows={2}
347-
/>
348-
</Box>
349-
350-
<Box sx={{ flex: 1, overflowY: 'auto', px: 3 }}>
351-
{selectedGroup && selectedGroup.tags.length > 0 ? (
352-
<List sx={{ p: 0 }} subheader={<ListSubheader>Tags</ListSubheader>}>
353-
{selectedGroup.tags.map((tag, idx) => (
354-
<ListItem key={tag.id} divider={idx < selectedGroup.tags.length - 1}>
355-
<ListItemText
356-
primary={tag.name}
357-
secondary={tag.description}
358-
sx={{ pr: 3, overflow: 'hidden' }}
359-
secondaryTypographyProps={{
360-
variant: 'subtitle2',
361-
style: {
362-
whiteSpace: 'nowrap',
363-
overflow: 'hidden',
364-
textOverflow: 'ellipsis',
365-
},
366-
}}
367-
/>
368-
<ListItemSecondaryAction>
369-
{onEditTag && (
370-
<IconButton
371-
edge="end"
372-
aria-label="edit tag"
373-
size="small"
374-
onClick={() => handleOpenTagDialog(tag)}
375-
color="default"
376-
>
377-
{isAdmin ? <Edit fontSize="small" /> : <NoteOutlined fontSize="small" />}
378-
</IconButton>
379-
)}
380-
{isAdmin && onDeleteTag && (
381-
<IconButton
382-
edge="end"
383-
aria-label="delete"
384-
size="small"
385-
onClick={() => handleDeleteTag(tag)}
386-
color="error"
387-
>
388-
<Delete fontSize="small" />
389-
</IconButton>
343+
disabled={!isAdmin}
344+
label="Description"
345+
value={groupDescription}
346+
onChange={(e) => setGroupDescription(e.target.value)}
347+
InputProps={{
348+
endAdornment: (
349+
<InputAdornment position="end">
350+
{editMode &&
351+
selectedGroup &&
352+
groupDescription !== selectedGroup.description && (
353+
<Tooltip title="Group description has unsaved changes">
354+
<NewReleasesOutlined color="warning" />
355+
</Tooltip>
390356
)}
391-
</ListItemSecondaryAction>
392-
</ListItem>
393-
))}
394-
</List>
395-
) : (
396-
<Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
397-
No tags in this group
398-
</Typography>
357+
</InputAdornment>
358+
),
359+
}}
360+
/>
361+
{isAdmin && (
362+
<DialogActions sx={{ px: 0 }}>
363+
<Button onClick={handleCloseDialog}>Cancel</Button>
364+
<Button variant="contained" onClick={handleSave}>
365+
Save
366+
</Button>
367+
</DialogActions>
399368
)}
400369
</Box>
401370

371+
{editMode && (
372+
<Box sx={{ flex: 1, overflowY: 'auto', px: 3 }}>
373+
{selectedGroup && selectedGroup.tags.length > 0 ? (
374+
<List sx={{ p: 0 }} subheader={<ListSubheader>Tags</ListSubheader>}>
375+
{selectedGroup.tags.map((tag, idx) => (
376+
<ListItem key={tag.id} divider={idx < selectedGroup.tags.length - 1}>
377+
<ListItemText
378+
primary={tag.name}
379+
secondary={tag.description}
380+
sx={{ pr: 3, overflow: 'hidden' }}
381+
secondaryTypographyProps={{
382+
variant: 'subtitle2',
383+
style: {
384+
whiteSpace: 'nowrap',
385+
overflow: 'hidden',
386+
textOverflow: 'ellipsis',
387+
},
388+
}}
389+
/>
390+
<ListItemSecondaryAction>
391+
{onEditTag && (
392+
<IconButton
393+
edge="end"
394+
aria-label="edit tag"
395+
size="small"
396+
onClick={() => handleOpenTagDialog(tag)}
397+
color="default"
398+
>
399+
{isAdmin ? (
400+
<Edit fontSize="small" />
401+
) : (
402+
<NoteOutlined fontSize="small" />
403+
)}
404+
</IconButton>
405+
)}
406+
{isAdmin && onDeleteTag && (
407+
<IconButton
408+
edge="end"
409+
aria-label="delete"
410+
size="small"
411+
onClick={() => handleDeleteTag(tag)}
412+
color="error"
413+
>
414+
<Delete fontSize="small" />
415+
</IconButton>
416+
)}
417+
</ListItemSecondaryAction>
418+
</ListItem>
419+
))}
420+
</List>
421+
) : (
422+
<Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
423+
No tags in this group
424+
</Typography>
425+
)}
426+
</Box>
427+
)}
428+
402429
{isAdmin && editMode && onAddTag && (
403430
<Box sx={{ px: 3, py: 2, borderTop: '1px solid #eee' }}>
404431
<Box sx={{ display: 'flex', gap: 1 }}>
@@ -407,7 +434,7 @@ export function TagPage({
407434
label="New Tag Name"
408435
value={newTagName}
409436
onChange={(e) => setNewTagName(e.target.value)}
410-
onKeyPress={(e) => {
437+
onKeyDown={(e) => {
411438
if (e.key === 'Enter') {
412439
e.preventDefault();
413440
handleAddNewTag();
@@ -427,14 +454,11 @@ export function TagPage({
427454
</Box>
428455
)}
429456
</DialogContent>
430-
<DialogActions>
431-
<Button onClick={handleCloseDialog}>{isAdmin ? 'Cancel' : 'Close'}</Button>
432-
{isAdmin && (
433-
<Button variant="contained" onClick={handleSave}>
434-
Save
435-
</Button>
436-
)}
437-
</DialogActions>
457+
{editMode && (
458+
<DialogActions>
459+
<Button onClick={handleCloseDialog}>Close</Button>
460+
</DialogActions>
461+
)}
438462
</Dialog>
439463

440464
{/* Tag Dialog */}

0 commit comments

Comments
 (0)