Skip to content

Commit f8fd2c0

Browse files
committed
Adds script editing and facility management UI
Introduces activity and step creation dialogs for multi-app training flows Enhances developer access checks for facility selection Incorporates user authentication to refine authorized queries Relates to #789
1 parent 3ae8c15 commit f8fd2c0

19 files changed

+2271
-0
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React, { useState } from 'react';
2+
3+
import { Activity } from '../types';
4+
import { createActivity } from '../factories';
5+
6+
interface ActivityDialogProps {
7+
open: boolean;
8+
onClose: () => void;
9+
onAdd: (activity: Activity) => void;
10+
}
11+
12+
export const ActivityDialog: React.FC<ActivityDialogProps> = ({ open, onClose, onAdd }) => {
13+
const [id, setId] = useState('');
14+
const [nodeType, setNodeType] = useState('RangeAnalysisScriptedActivity');
15+
const [header, setHeader] = useState('');
16+
const [description, setDescription] = useState('');
17+
if (!open) return null;
18+
return (
19+
<div className="modal-bg">
20+
<div className="modal dialog">
21+
<h3 className="dialog-title">Add Activity</h3>
22+
<div className="dialog-body">
23+
<div className="field">
24+
<label className="field-label">ID</label>
25+
<input
26+
className="field-input"
27+
placeholder="unique-activity-id"
28+
value={id}
29+
onChange={e => setId(e.target.value)}
30+
/>
31+
<div className="field-hint">Lowercase or camelCase identifier (no spaces)</div>
32+
</div>
33+
<div className="field">
34+
<label className="field-label">Type</label>
35+
<select aria-label="Activity Type" className="field-input" value={nodeType} onChange={e => setNodeType(e.target.value)}>
36+
<option value="RangeAnalysisScriptedActivity">RangeAnalysisScriptedActivity</option>
37+
<option value="PerformanceCenterScriptedActivity">PerformanceCenterScriptedActivity</option>
38+
</select>
39+
<div className="field-hint">Choose the activity implementation class</div>
40+
</div>
41+
<div className="field">
42+
<label className="field-label">Intro Header</label>
43+
<input
44+
className="field-input"
45+
placeholder="e.g. Range Analysis Warmup"
46+
value={header}
47+
onChange={e => setHeader(e.target.value)}
48+
/>
49+
<div className="field-hint">Short headline shown to the user</div>
50+
</div>
51+
<div className="field">
52+
<label className="field-label">Description</label>
53+
<textarea
54+
className="field-input field-textarea"
55+
placeholder="What the player should focus on"
56+
value={description}
57+
rows={5}
58+
onChange={e => setDescription(e.target.value)}
59+
/>
60+
<div className="field-hint">Optional supporting text (multi-line)</div>
61+
</div>
62+
</div>
63+
<div className="dialog-footer">
64+
<button
65+
disabled={!id || !header}
66+
onClick={() => {
67+
const activity: Activity = createActivity({
68+
nodeType: nodeType as any,
69+
id,
70+
introHeader: header,
71+
introDescription: description,
72+
});
73+
onAdd(activity);
74+
onClose();
75+
}}
76+
>Add Activity</button>
77+
<button className="secondary" onClick={onClose}>Cancel</button>
78+
</div>
79+
</div>
80+
</div>
81+
);
82+
};
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React from 'react';
2+
import { useAuth } from '../lib/AuthProvider';
3+
4+
export const AuthStatus: React.FC = () => {
5+
const { isAuthenticated, isLoading, error, login, logout, refreshAuth, tokenInfo } = useAuth();
6+
7+
if (isLoading) {
8+
return (
9+
<div className="auth-status loading">
10+
<span>🔄 Checking authentication...</span>
11+
</div>
12+
);
13+
}
14+
15+
return (
16+
<div className="auth-status">
17+
<div className="auth-info">
18+
<span className={`status-indicator ${isAuthenticated ? 'authenticated' : 'unauthenticated'}`}>
19+
{isAuthenticated ? '🔒 Authenticated' : '🔓 Not Authenticated'}
20+
</span>
21+
22+
{tokenInfo.expiresAt && (
23+
<span className="token-expiry">
24+
Expires: {tokenInfo.expiresAt.toLocaleString()}
25+
</span>
26+
)}
27+
28+
{tokenInfo.scope && (
29+
<span className="token-scope">
30+
Scope: {tokenInfo.scope}
31+
</span>
32+
)}
33+
</div>
34+
35+
<div className="auth-actions">
36+
{!isAuthenticated ? (
37+
<button onClick={login} className="auth-button login">
38+
Login
39+
</button>
40+
) : (
41+
<>
42+
<button onClick={refreshAuth} className="auth-button refresh">
43+
Refresh Token
44+
</button>
45+
<button onClick={logout} className="auth-button logout">
46+
Logout
47+
</button>
48+
</>
49+
)}
50+
</div>
51+
52+
{error && (
53+
<div className="auth-error">
54+
⚠️ {error}
55+
</div>
56+
)}
57+
</div>
58+
);
59+
};
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import React, { useState, useEffect, useRef } from 'react';
2+
import { useQuery } from 'urql';
3+
import { BAYS_IN_LOCATION_QUERY } from '../graphql/queries';
4+
5+
interface Bay {
6+
id: string;
7+
dbId: number;
8+
name: string;
9+
}
10+
11+
interface BayQueryData {
12+
node: {
13+
bays: Bay[];
14+
} | null;
15+
}
16+
17+
interface BaySelectorProps {
18+
selectedFacilityId: string | null;
19+
selectedLocationId: string | null;
20+
selectedBayId: string | null;
21+
persistedBayId: string | null;
22+
onBaySelect: (bay: Bay | null) => void;
23+
}
24+
25+
export const BaySelector: React.FC<BaySelectorProps> = ({
26+
selectedFacilityId,
27+
selectedLocationId,
28+
selectedBayId,
29+
persistedBayId,
30+
onBaySelect,
31+
}) => {
32+
const [isOpen, setIsOpen] = useState(false);
33+
34+
const [result] = useQuery<BayQueryData>({
35+
query: BAYS_IN_LOCATION_QUERY,
36+
variables: { locationId: selectedLocationId },
37+
pause: !selectedLocationId, // Don't run query if no location is selected
38+
requestPolicy: 'cache-and-network', // Always check for fresh data
39+
});
40+
41+
const { data, fetching, error } = result;
42+
const bays = data?.node?.bays || [];
43+
const selectedBay = bays.find(bay => bay.id === selectedBayId);
44+
45+
// Clear bay when facility or location changes
46+
const prevFacilityId = useRef<string | null>(null);
47+
const prevLocationId = useRef<string | null>(null);
48+
49+
useEffect(() => {
50+
// Clear bay if facility changed (facility change cascades down)
51+
if (prevFacilityId.current !== null && prevFacilityId.current !== selectedFacilityId) {
52+
onBaySelect(null);
53+
}
54+
prevFacilityId.current = selectedFacilityId;
55+
}, [selectedFacilityId, onBaySelect]);
56+
57+
useEffect(() => {
58+
// Clear bay if location changed
59+
if (prevLocationId.current !== null && prevLocationId.current !== selectedLocationId) {
60+
onBaySelect(null);
61+
}
62+
prevLocationId.current = selectedLocationId;
63+
}, [selectedLocationId, onBaySelect]);
64+
65+
// Restore persisted bay selection when bays are loaded
66+
useEffect(() => {
67+
if (bays.length > 0 && persistedBayId && !selectedBayId) {
68+
const persistedBay = bays.find(bay => bay.id === persistedBayId);
69+
if (persistedBay) {
70+
onBaySelect(persistedBay);
71+
console.log('Restored bay selection from persistence:', persistedBay);
72+
}
73+
}
74+
}, [bays, persistedBayId, selectedBayId, onBaySelect]);
75+
76+
const handleBaySelect = (bay: Bay) => {
77+
onBaySelect(bay);
78+
setIsOpen(false);
79+
};
80+
81+
const handleClearSelection = () => {
82+
onBaySelect(null);
83+
setIsOpen(false);
84+
};
85+
86+
if (!selectedLocationId) {
87+
return (
88+
<div className="bay-selector">
89+
<div className="bay-selector-button disabled">
90+
Select a location first
91+
</div>
92+
</div>
93+
);
94+
}
95+
96+
if (fetching) {
97+
return (
98+
<div className="bay-selector">
99+
<div className="bay-selector-button loading">
100+
Loading bays...
101+
</div>
102+
</div>
103+
);
104+
}
105+
106+
if (error) {
107+
return (
108+
<div className="bay-selector">
109+
<div className="bay-selector-button error">
110+
Error loading bays
111+
</div>
112+
</div>
113+
);
114+
}
115+
116+
return (
117+
<div className="bay-selector">
118+
<button
119+
className={`bay-selector-button ${isOpen ? 'open' : ''}`}
120+
onClick={() => setIsOpen(!isOpen)}
121+
>
122+
{selectedBay ? selectedBay.name : 'Select Bay'}
123+
<svg
124+
className={`bay-selector-arrow ${isOpen ? 'open' : ''}`}
125+
width="16"
126+
height="16"
127+
viewBox="0 0 16 16"
128+
fill="currentColor"
129+
>
130+
<path d="M8 12L3 7L4 6L8 10L12 6L13 7L8 12Z" />
131+
</svg>
132+
</button>
133+
134+
{isOpen && (
135+
<div className="bay-dropdown">
136+
{selectedBay && (
137+
<div className="bay-item clear-selection" onClick={handleClearSelection}>
138+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
139+
<path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"/>
140+
</svg>
141+
Clear Selection
142+
</div>
143+
)}
144+
145+
{bays.length === 0 ? (
146+
<div className="bay-item no-bays">
147+
No bays available
148+
</div>
149+
) : (
150+
bays.map((bay) => (
151+
<div
152+
key={bay.id}
153+
className={`bay-item ${selectedBay?.id === bay.id ? 'selected' : ''}`}
154+
onClick={() => handleBaySelect(bay)}
155+
>
156+
<div className="bay-info">
157+
<div className="bay-name">{bay.name}</div>
158+
</div>
159+
{selectedBay?.id === bay.id && (
160+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" className="checkmark">
161+
<path d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"/>
162+
</svg>
163+
)}
164+
</div>
165+
))
166+
)}
167+
</div>
168+
)}
169+
</div>
170+
);
171+
};
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React from 'react';
2+
import { ConditionGroup, ParameterCondition } from '../types';
3+
4+
interface ConditionEditorProps {
5+
label: string;
6+
condition: ConditionGroup | undefined;
7+
onChange: (c: ConditionGroup) => void;
8+
showConditionType?: boolean;
9+
}
10+
11+
export const ConditionEditor: React.FC<ConditionEditorProps> = ({ label, condition, onChange, showConditionType }) => {
12+
const c: ConditionGroup = condition || { shots: 0, conditions: [] };
13+
14+
const update = (patch: Partial<ConditionGroup>) => {
15+
onChange({ ...c, ...patch });
16+
};
17+
18+
const updateConditionRow = (idx: number, patch: Partial<ParameterCondition>) => {
19+
const rows = [...(c.conditions || [])];
20+
rows[idx] = { ...rows[idx], ...patch };
21+
update({ conditions: rows });
22+
};
23+
24+
const addRow = () => {
25+
update({ conditions: [...(c.conditions || []), { parameter: '', min: undefined, max: undefined }] });
26+
};
27+
28+
const removeRow = (idx: number) => {
29+
const rows = [...(c.conditions || [])];
30+
rows.splice(idx, 1);
31+
update({ conditions: rows });
32+
};
33+
34+
return (
35+
<div className="cond-editor">
36+
<h4 className="cond-title">{label}</h4>
37+
<label className="cond-label">Shots: <input className="cond-input-narrow" type="number" value={c.shots ?? ''} onChange={e => update({ shots: Number(e.target.value) })} /></label>
38+
{showConditionType && (
39+
<label className="cond-label">Condition Type:
40+
<select className="cond-select" value={c.conditionType || ''} onChange={e => update({ conditionType: e.target.value as any })}>
41+
<option value="">(none)</option>
42+
<option value="And">And</option>
43+
<option value="Or">Or</option>
44+
</select>
45+
</label>
46+
)}
47+
<div className="cond-conditions">
48+
<strong>Conditions</strong>
49+
{(c.conditions || []).length === 0 && <div className="cond-empty">No parameter constraints</div>}
50+
{(c.conditions || []).map((row, i) => (
51+
<div key={i} className="cond-row">
52+
<input className="cond-input" placeholder="parameter" value={row.parameter || ''} onChange={e => updateConditionRow(i, { parameter: e.target.value })} />
53+
<input className="cond-input-narrow" placeholder="min" type="number" value={row.min ?? ''} onChange={e => updateConditionRow(i, { min: e.target.value === '' ? undefined : Number(e.target.value) })} />
54+
<input className="cond-input-narrow" placeholder="max" type="number" value={row.max ?? ''} onChange={e => updateConditionRow(i, { max: e.target.value === '' ? undefined : Number(e.target.value) })} />
55+
<button className="cond-remove" onClick={() => removeRow(i)}></button>
56+
</div>
57+
))}
58+
<button className="cond-add" onClick={addRow}>Add Condition</button>
59+
</div>
60+
</div>
61+
);
62+
};

0 commit comments

Comments
 (0)