@checkstack/catalog-frontend 0.3.10 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # @checkstack/catalog-frontend
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e5079e1: Add contacts management to system editor
8
+
9
+ - **catalog-frontend**: New `ContactsEditor` component allows adding/removing platform users and external mailboxes as system contacts directly from the system editor dialog
10
+ - **catalog-common**: Added `instanceAccess` override to contacts RPC endpoints for correct single-resource RLAC checking
11
+ - **ui**: Fixed Tabs component to use `type="button"` to prevent form submission when used inside forms
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [e5079e1]
16
+ - Updated dependencies [9551fd7]
17
+ - @checkstack/catalog-common@1.2.6
18
+ - @checkstack/ui@0.5.3
19
+ - @checkstack/auth-frontend@0.5.8
20
+
21
+ ## 0.3.11
22
+
23
+ ### Patch Changes
24
+
25
+ - 0b9fc58: Fix workspace:\* protocol resolution in published packages
26
+
27
+ Published packages now correctly have resolved dependency versions instead of `workspace:*` references. This is achieved by using `bun publish` which properly resolves workspace protocol references.
28
+
29
+ - Updated dependencies [0b9fc58]
30
+ - @checkstack/auth-frontend@0.5.7
31
+ - @checkstack/catalog-common@1.2.5
32
+ - @checkstack/common@0.6.1
33
+ - @checkstack/frontend-api@0.3.4
34
+ - @checkstack/notification-common@0.2.4
35
+ - @checkstack/ui@0.5.2
36
+
3
37
  ## 0.3.10
4
38
 
5
39
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/catalog-frontend",
3
- "version": "0.3.10",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "main": "src/index.tsx",
6
6
  "scripts": {
@@ -9,12 +9,13 @@
9
9
  "lint:code": "eslint . --max-warnings 0"
10
10
  },
11
11
  "dependencies": {
12
- "@checkstack/catalog-common": "workspace:*",
13
- "@checkstack/frontend-api": "workspace:*",
14
- "@checkstack/auth-frontend": "workspace:*",
15
- "@checkstack/common": "workspace:*",
16
- "@checkstack/notification-common": "workspace:*",
17
- "@checkstack/ui": "workspace:*",
12
+ "@checkstack/auth-common": "0.5.4",
13
+ "@checkstack/catalog-common": "1.2.5",
14
+ "@checkstack/frontend-api": "0.3.4",
15
+ "@checkstack/auth-frontend": "0.5.7",
16
+ "@checkstack/common": "0.6.1",
17
+ "@checkstack/notification-common": "0.2.4",
18
+ "@checkstack/ui": "0.5.2",
18
19
  "react": "^18.2.0",
19
20
  "react-router-dom": "^6.22.0",
20
21
  "lucide-react": "^0.344.0"
@@ -22,7 +23,7 @@
22
23
  "devDependencies": {
23
24
  "typescript": "^5.0.0",
24
25
  "@types/react": "^18.2.0",
25
- "@checkstack/tsconfig": "workspace:*",
26
- "@checkstack/scripts": "workspace:*"
26
+ "@checkstack/tsconfig": "0.0.3",
27
+ "@checkstack/scripts": "0.1.1"
27
28
  }
28
29
  }
@@ -0,0 +1,296 @@
1
+ import React, { useState } from "react";
2
+ import {
3
+ Button,
4
+ Input,
5
+ Label,
6
+ useToast,
7
+ LoadingSpinner,
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ Tabs,
14
+ TabPanel,
15
+ } from "@checkstack/ui";
16
+ import {
17
+ usePluginClient,
18
+ useApi,
19
+ accessApiRef,
20
+ } from "@checkstack/frontend-api";
21
+ import { CatalogApi, type SystemContact } from "@checkstack/catalog-common";
22
+ import { AuthApi, authAccess } from "@checkstack/auth-common";
23
+ import { User, Mail, Trash2, Plus } from "lucide-react";
24
+
25
+ interface ContactsEditorProps {
26
+ systemId: string;
27
+ }
28
+
29
+ interface UserDto {
30
+ id: string;
31
+ name: string;
32
+ email: string;
33
+ }
34
+
35
+ export const ContactsEditor: React.FC<ContactsEditorProps> = ({ systemId }) => {
36
+ const catalogClient = usePluginClient(CatalogApi);
37
+ const authClient = usePluginClient(AuthApi);
38
+ const accessApi = useApi(accessApiRef);
39
+ const toast = useToast();
40
+
41
+ // Check if user can search users
42
+ const { allowed: canSearchUsers, loading: accessLoading } =
43
+ accessApi.useAccess(authAccess.users.read);
44
+
45
+ // Form state
46
+ const [selectedUserId, setSelectedUserId] = useState("");
47
+ const [mailboxEmail, setMailboxEmail] = useState("");
48
+ const [label, setLabel] = useState("");
49
+ const [activeTab, setActiveTab] = useState("mailbox");
50
+
51
+ // Update active tab when permission loading completes
52
+ React.useEffect(() => {
53
+ if (!accessLoading && canSearchUsers) {
54
+ setActiveTab("user");
55
+ }
56
+ }, [accessLoading, canSearchUsers]);
57
+
58
+ // Fetch existing contacts
59
+ const {
60
+ data: contacts = [],
61
+ isLoading: contactsLoading,
62
+ refetch: refetchContacts,
63
+ } = catalogClient.getSystemContacts.useQuery({ systemId });
64
+
65
+ // Fetch users for selection (only if user has permission)
66
+ const { data: users = [] } = authClient.getUsers.useQuery(
67
+ {},
68
+ { enabled: canSearchUsers },
69
+ );
70
+
71
+ // Add contact mutation
72
+ const addContactMutation = catalogClient.addSystemContact.useMutation({
73
+ onSuccess: () => {
74
+ toast.success("Contact added successfully");
75
+ setSelectedUserId("");
76
+ setMailboxEmail("");
77
+ setLabel("");
78
+ void refetchContacts();
79
+ },
80
+ onError: (error) => {
81
+ toast.error(
82
+ error instanceof Error ? error.message : "Failed to add contact",
83
+ );
84
+ },
85
+ });
86
+
87
+ // Remove contact mutation
88
+ const removeContactMutation = catalogClient.removeSystemContact.useMutation({
89
+ onSuccess: () => {
90
+ toast.success("Contact removed");
91
+ void refetchContacts();
92
+ },
93
+ onError: (error) => {
94
+ toast.error(
95
+ error instanceof Error ? error.message : "Failed to remove contact",
96
+ );
97
+ },
98
+ });
99
+
100
+ const handleAddUserContact = () => {
101
+ if (!selectedUserId) {
102
+ toast.error("Please select a user");
103
+ return;
104
+ }
105
+
106
+ addContactMutation.mutate({
107
+ systemId,
108
+ type: "user",
109
+ userId: selectedUserId,
110
+ label: label.trim() || undefined,
111
+ });
112
+ };
113
+
114
+ const handleAddMailboxContact = () => {
115
+ if (!mailboxEmail.trim()) {
116
+ toast.error("Please enter an email address");
117
+ return;
118
+ }
119
+
120
+ // Basic email validation
121
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(mailboxEmail.trim())) {
122
+ toast.error("Please enter a valid email address");
123
+ return;
124
+ }
125
+
126
+ addContactMutation.mutate({
127
+ systemId,
128
+ type: "mailbox",
129
+ email: mailboxEmail.trim(),
130
+ label: label.trim() || undefined,
131
+ });
132
+ };
133
+
134
+ const handleRemoveContact = (contactId: string) => {
135
+ removeContactMutation.mutate(contactId);
136
+ };
137
+
138
+ // Filter out users who are already contacts
139
+ const existingUserIds = new Set(
140
+ contacts
141
+ .filter((c): c is SystemContact & { type: "user" } => c.type === "user")
142
+ .map((c) => c.userId),
143
+ );
144
+ const availableUsers = (users as UserDto[]).filter(
145
+ (u) => !existingUserIds.has(u.id),
146
+ );
147
+
148
+ // Build tab items
149
+ const tabItems = [];
150
+ if (canSearchUsers) {
151
+ tabItems.push({ id: "user", label: "Add User" });
152
+ }
153
+ tabItems.push({ id: "mailbox", label: "Add Mailbox" });
154
+
155
+ if (contactsLoading || accessLoading) {
156
+ return (
157
+ <div className="flex justify-center py-4">
158
+ <LoadingSpinner />
159
+ </div>
160
+ );
161
+ }
162
+
163
+ return (
164
+ <div className="space-y-4">
165
+ <Label>Contacts</Label>
166
+
167
+ {/* Existing Contacts List */}
168
+ {contacts.length > 0 && (
169
+ <div className="border rounded-lg divide-y">
170
+ {contacts.map((contact) => (
171
+ <div
172
+ key={contact.id}
173
+ className="flex items-center justify-between p-3"
174
+ >
175
+ <div className="flex items-center gap-2">
176
+ {contact.type === "user" ? (
177
+ <User className="h-4 w-4 text-muted-foreground" />
178
+ ) : (
179
+ <Mail className="h-4 w-4 text-muted-foreground" />
180
+ )}
181
+ <div>
182
+ <span className="text-sm">
183
+ {contact.type === "user"
184
+ ? (contact.userName ?? contact.userId)
185
+ : contact.email}
186
+ </span>
187
+ {contact.label && (
188
+ <span className="text-xs text-muted-foreground ml-2">
189
+ ({contact.label})
190
+ </span>
191
+ )}
192
+ </div>
193
+ </div>
194
+ <Button
195
+ variant="ghost"
196
+ size="sm"
197
+ onClick={() => handleRemoveContact(contact.id)}
198
+ disabled={removeContactMutation.isPending}
199
+ >
200
+ <Trash2 className="h-4 w-4" />
201
+ </Button>
202
+ </div>
203
+ ))}
204
+ </div>
205
+ )}
206
+
207
+ {contacts.length === 0 && (
208
+ <p className="text-sm text-muted-foreground">
209
+ No contacts assigned yet
210
+ </p>
211
+ )}
212
+
213
+ {/* Add Contact Section */}
214
+ <div className="border rounded-lg p-4 space-y-4">
215
+ <Tabs
216
+ items={tabItems}
217
+ activeTab={activeTab}
218
+ onTabChange={setActiveTab}
219
+ />
220
+
221
+ {canSearchUsers && (
222
+ <TabPanel id="user" activeTab={activeTab} className="space-y-3 pt-3">
223
+ <div className="space-y-2">
224
+ <Label htmlFor="user-select">Platform User</Label>
225
+ <Select value={selectedUserId} onValueChange={setSelectedUserId}>
226
+ <SelectTrigger id="user-select">
227
+ <SelectValue placeholder="Select a user" />
228
+ </SelectTrigger>
229
+ <SelectContent>
230
+ {availableUsers.length === 0 ? (
231
+ <SelectItem value="_none" disabled>
232
+ No available users
233
+ </SelectItem>
234
+ ) : (
235
+ availableUsers.map((user) => (
236
+ <SelectItem key={user.id} value={user.id}>
237
+ {user.name} ({user.email})
238
+ </SelectItem>
239
+ ))
240
+ )}
241
+ </SelectContent>
242
+ </Select>
243
+ </div>
244
+ <div className="space-y-2">
245
+ <Label htmlFor="user-label">Label (optional)</Label>
246
+ <Input
247
+ id="user-label"
248
+ placeholder="e.g., Primary, On-Call"
249
+ value={label}
250
+ onChange={(e) => setLabel(e.target.value)}
251
+ />
252
+ </div>
253
+ <Button
254
+ onClick={handleAddUserContact}
255
+ disabled={!selectedUserId || addContactMutation.isPending}
256
+ size="sm"
257
+ >
258
+ <Plus className="h-4 w-4 mr-1" />
259
+ Add User Contact
260
+ </Button>
261
+ </TabPanel>
262
+ )}
263
+
264
+ <TabPanel id="mailbox" activeTab={activeTab} className="space-y-3 pt-3">
265
+ <div className="space-y-2">
266
+ <Label htmlFor="mailbox-email">Email Address</Label>
267
+ <Input
268
+ id="mailbox-email"
269
+ type="email"
270
+ placeholder="team@example.com"
271
+ value={mailboxEmail}
272
+ onChange={(e) => setMailboxEmail(e.target.value)}
273
+ />
274
+ </div>
275
+ <div className="space-y-2">
276
+ <Label htmlFor="mailbox-label">Label (optional)</Label>
277
+ <Input
278
+ id="mailbox-label"
279
+ placeholder="e.g., Support, Escalation"
280
+ value={label}
281
+ onChange={(e) => setLabel(e.target.value)}
282
+ />
283
+ </div>
284
+ <Button
285
+ onClick={handleAddMailboxContact}
286
+ disabled={!mailboxEmail.trim() || addContactMutation.isPending}
287
+ size="sm"
288
+ >
289
+ <Plus className="h-4 w-4 mr-1" />
290
+ Add Mailbox Contact
291
+ </Button>
292
+ </TabPanel>
293
+ </div>
294
+ </div>
295
+ );
296
+ };
@@ -24,7 +24,15 @@ import {
24
24
  } from "@checkstack/ui";
25
25
  import { authApiRef } from "@checkstack/auth-frontend/api";
26
26
 
27
- import { Activity, Info, Users, FileJson, Calendar } from "lucide-react";
27
+ import {
28
+ Activity,
29
+ Info,
30
+ Users,
31
+ FileJson,
32
+ Calendar,
33
+ Mail,
34
+ User,
35
+ } from "lucide-react";
28
36
 
29
37
  const CATALOG_PLUGIN_ID = "catalog";
30
38
 
@@ -57,6 +65,12 @@ export const SystemDetailPage: React.FC = () => {
57
65
  const { data: groupsData, isLoading: groupsLoading } =
58
66
  catalogClient.getGroups.useQuery({});
59
67
 
68
+ // Fetch contacts for this system
69
+ const { data: contactsData } = catalogClient.getSystemContacts.useQuery(
70
+ { systemId: systemId ?? "" },
71
+ { enabled: !!systemId },
72
+ );
73
+
60
74
  // Find the system from the fetched data
61
75
  const system = systemsData?.systems.find((s) => s.id === systemId);
62
76
  const loading = systemsLoading || groupsLoading;
@@ -218,14 +232,6 @@ export const SystemDetailPage: React.FC = () => {
218
232
  {system.description || "No description provided"}
219
233
  </p>
220
234
  </div>
221
- <div>
222
- <label className="text-sm font-medium text-muted-foreground">
223
- Owner
224
- </label>
225
- <p className="mt-1 text-foreground">
226
- {system.owner || "Not assigned"}
227
- </p>
228
- </div>
229
235
  <div className="flex gap-6 text-sm">
230
236
  <div className="flex items-center gap-2 text-muted-foreground">
231
237
  <Calendar className="h-4 w-4" />
@@ -253,6 +259,51 @@ export const SystemDetailPage: React.FC = () => {
253
259
  </CardContent>
254
260
  </Card>
255
261
 
262
+ {/* Contacts Card */}
263
+ <Card className="border-border shadow-sm">
264
+ <CardHeader className="border-b border-border bg-muted/30">
265
+ <div className="flex items-center gap-2">
266
+ <Mail className="h-5 w-5 text-muted-foreground" />
267
+ <CardTitle className="text-lg font-semibold">Contacts</CardTitle>
268
+ </div>
269
+ </CardHeader>
270
+ <CardContent className="p-6">
271
+ {!contactsData || contactsData.length === 0 ? (
272
+ <p className="text-muted-foreground text-sm">
273
+ No contacts assigned to this system
274
+ </p>
275
+ ) : (
276
+ <div className="space-y-2">
277
+ {contactsData.map((contact) => (
278
+ <div
279
+ key={contact.id}
280
+ className="flex items-center gap-2 text-sm"
281
+ >
282
+ {contact.type === "user" ? (
283
+ <User className="h-4 w-4 text-muted-foreground" />
284
+ ) : (
285
+ <Mail className="h-4 w-4 text-muted-foreground" />
286
+ )}
287
+ <a
288
+ href={`mailto:${contact.type === "user" ? contact.userEmail : contact.email}`}
289
+ className="text-primary hover:underline"
290
+ >
291
+ {contact.type === "user"
292
+ ? (contact.userName ?? contact.userId)
293
+ : contact.email}
294
+ </a>
295
+ {contact.label && (
296
+ <span className="text-muted-foreground">
297
+ ({contact.label})
298
+ </span>
299
+ )}
300
+ </div>
301
+ ))}
302
+ </div>
303
+ )}
304
+ </CardContent>
305
+ </Card>
306
+
256
307
  {/* Groups Card */}
257
308
  <Card className="border-border shadow-sm">
258
309
  <CardHeader className="border-b border-border bg-muted/30">
@@ -12,6 +12,7 @@ import {
12
12
  useToast,
13
13
  } from "@checkstack/ui";
14
14
  import { TeamAccessEditor } from "@checkstack/auth-frontend";
15
+ import { ContactsEditor } from "./ContactsEditor";
15
16
 
16
17
  interface SystemEditorProps {
17
18
  open: boolean;
@@ -28,7 +29,7 @@ export const SystemEditor: React.FC<SystemEditorProps> = ({
28
29
  }) => {
29
30
  const [name, setName] = useState(initialData?.name || "");
30
31
  const [description, setDescription] = useState(
31
- initialData?.description || ""
32
+ initialData?.description || "",
32
33
  );
33
34
  const [loading, setLoading] = useState(false);
34
35
  const toast = useToast();
@@ -100,6 +101,9 @@ export const SystemEditor: React.FC<SystemEditorProps> = ({
100
101
  />
101
102
  </div>
102
103
 
104
+ {/* Contacts Editor - only shown for existing systems */}
105
+ {initialData?.id && <ContactsEditor systemId={initialData.id} />}
106
+
103
107
  {/* Team Access Editor - only shown for existing systems */}
104
108
  {initialData?.id && (
105
109
  <TeamAccessEditor
@@ -119,8 +123,8 @@ export const SystemEditor: React.FC<SystemEditorProps> = ({
119
123
  {loading
120
124
  ? "Saving..."
121
125
  : initialData
122
- ? "Save Changes"
123
- : "Create System"}
126
+ ? "Save Changes"
127
+ : "Create System"}
124
128
  </Button>
125
129
  </DialogFooter>
126
130
  </form>