@stacksjs/defaults 0.75.264 → 0.75.267
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/ai/skills/stacks-browse/SKILL.md +2 -0
- package/ai/skills/stacks-browse/scripts/browse.ts +33 -10
- package/ai/skills/stacks-chat/SKILL.md +32 -0
- package/ai/skills/stacks-native/CATALOG.md +8 -8
- package/ide/vscode/package.json +1 -1
- package/package.json +4 -4
- package/resources/components/Dashboard/App/RequestsOverview.stx +75 -58
- package/resources/components/Dashboard/App/SourceInventory.stx +68 -59
- package/resources/components/Dashboard/Infrastructure/DnsDashboard.stx +46 -28
- package/resources/components/Dashboard/Releases/ReleaseDashboard.stx +35 -61
- package/resources/components/Dashboard/Teams/TeamPeopleDashboard.stx +127 -143
|
@@ -37,6 +37,8 @@ A candidate is validated with `--version` before use, so a dead wrapper (e.g. a
|
|
|
37
37
|
shim pointing at an uninstalled `.app`) is skipped automatically. If none launch, it tells
|
|
38
38
|
you to `brew install --cask chromium` or set `BROWSE_BROWSER`.
|
|
39
39
|
|
|
40
|
+
Startup waits up to 15 seconds for the running browser's valid DevTools port. An exited process or expired deadline fails launch; page readiness and scenario assertions have separate budgets.
|
|
41
|
+
|
|
40
42
|
## Default Stacks dev URLs
|
|
41
43
|
|
|
42
44
|
- Frontend: `http://localhost:3000`
|
|
@@ -174,6 +174,28 @@ class Cdp {
|
|
|
174
174
|
|
|
175
175
|
interface Session { proc: ReturnType<typeof spawn>, port: number, userDataDir: string, browser: string }
|
|
176
176
|
|
|
177
|
+
export async function waitForBrowserPort(proc: { exitCode: number | null }, portFile: string, timeoutMs: number): Promise<number | null> {
|
|
178
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 30_000)
|
|
179
|
+
throw new TypeError('Browser startup timeout must be an integer between 1 and 30000.')
|
|
180
|
+
const deadline = performance.now() + timeoutMs
|
|
181
|
+
while (performance.now() < deadline && proc.exitCode === null) {
|
|
182
|
+
try {
|
|
183
|
+
const line = readFileSync(portFile, 'utf8').split('\n')[0]?.trim()
|
|
184
|
+
const port = Number(line)
|
|
185
|
+
if (Number.isInteger(port) && port > 0 && port <= 65535) {
|
|
186
|
+
if (performance.now() >= deadline || proc.exitCode !== null) return null
|
|
187
|
+
return port
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
|
192
|
+
}
|
|
193
|
+
const remaining = deadline - performance.now()
|
|
194
|
+
if (remaining > 0) await Bun.sleep(Math.min(50, remaining))
|
|
195
|
+
}
|
|
196
|
+
return null
|
|
197
|
+
}
|
|
198
|
+
|
|
177
199
|
async function tryLaunch(browser: string): Promise<Session | null> {
|
|
178
200
|
const userDataDir = join(tmpdir(), `stacks-browse-${process.pid}-${Math.floor(Number(process.hrtime.bigint() % 1000000n))}`)
|
|
179
201
|
mkdirSync(userDataDir, { recursive: true })
|
|
@@ -196,18 +218,19 @@ async function tryLaunch(browser: string): Promise<Session | null> {
|
|
|
196
218
|
|
|
197
219
|
// Chromium writes the chosen port to <user-data-dir>/DevToolsActivePort.
|
|
198
220
|
const portFile = join(userDataDir, 'DevToolsActivePort')
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
221
|
+
let port: number | null = null
|
|
222
|
+
try {
|
|
223
|
+
port = await waitForBrowserPort(proc, portFile, 15_000)
|
|
224
|
+
if (port !== null) return { proc, port, userDataDir, browser }
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
if (port === null) {
|
|
228
|
+
try { proc.kill() }
|
|
229
|
+
catch { /* ignore */ }
|
|
230
|
+
try { rmSync(userDataDir, { recursive: true, force: true }) }
|
|
231
|
+
catch { /* a failed launch must not mask the next browser candidate */ }
|
|
204
232
|
}
|
|
205
|
-
await Bun.sleep(50)
|
|
206
233
|
}
|
|
207
|
-
try { proc.kill() }
|
|
208
|
-
catch { /* ignore */ }
|
|
209
|
-
try { rmSync(userDataDir, { recursive: true, force: true }) }
|
|
210
|
-
catch { /* a failed launch must not mask the next browser candidate */ }
|
|
211
234
|
return null
|
|
212
235
|
}
|
|
213
236
|
|
|
@@ -508,6 +508,38 @@ History is chronological, with `has_more` and a `before` message-id cursor.
|
|
|
508
508
|
Receipts acknowledge only messages through the id actually displayed.
|
|
509
509
|
`MessagingError.status` provides safe HTTP statuses for application handlers.
|
|
510
510
|
|
|
511
|
+
### Attachments
|
|
512
|
+
|
|
513
|
+
`send(id, body, retryKey, attachments?)` records files the application has
|
|
514
|
+
already stored. The trait keeps a reference, never bytes: `file` is the
|
|
515
|
+
application's own name for the upload (a disk path, a bucket key, an id), and
|
|
516
|
+
`type`, `size`, `width` and `height` are optional. Resolving and authorizing
|
|
517
|
+
the file stays with the application, so a disk, a bucket and a CDN look the
|
|
518
|
+
same from here.
|
|
519
|
+
|
|
520
|
+
```ts
|
|
521
|
+
import type { DirectMessageAttachment } from '@stacksjs/chat'
|
|
522
|
+
|
|
523
|
+
const photo: DirectMessageAttachment = { file: upload.key, type: 'image/jpeg', size: upload.size, width: 1200, height: 900 }
|
|
524
|
+
await messenger.send(thread.id, '', retryKey, [photo])
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
- A photo with no caption is a message: `body` may be empty when there is at
|
|
528
|
+
least one attachment, and only then.
|
|
529
|
+
- `message.attachments` is always an array, `[]` for text. Rows written before
|
|
530
|
+
the column existed read as `[]`.
|
|
531
|
+
- A retry key covers the attachments too. The same key with a different file
|
|
532
|
+
is a different message and returns 409, rather than silently keeping the
|
|
533
|
+
first photo.
|
|
534
|
+
- At most 10 attachments, 4000 characters once serialised. Keys the trait does
|
|
535
|
+
not know are dropped, so the column cannot become a second message body.
|
|
536
|
+
- The column is `chat_messages.attachments`, added by `buddy migrate` to a
|
|
537
|
+
database created before it. A database that has not migrated yet still sends
|
|
538
|
+
text.
|
|
539
|
+
- Notification previews are the application's to word: `onMessageSent`
|
|
540
|
+
receives the message with its attachments, so a photo with no caption can
|
|
541
|
+
read as "Sent a photo" rather than as an empty line.
|
|
542
|
+
|
|
511
543
|
For the UI, STX ships `ChatInbox`, `ChatMessage`, and `ChatUnreadBadge` in
|
|
512
544
|
`@stacksjs/components`. Browser stores import `createChat` from
|
|
513
545
|
`@stacksjs/components/chat` and inject their authenticated HTTP transport.
|
|
@@ -18,10 +18,10 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
|
|
|
18
18
|
| `@stacksjs/api` | [`stacks-api`](../stacks-api/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/api/src/index.ts) | 9 |
|
|
19
19
|
| `@stacksjs/arrays` | [`stacks-arrays`](../stacks-arrays/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/arrays/src/index.ts) | 6 |
|
|
20
20
|
| `@stacksjs/audio` | [`stacks-audio`](../stacks-audio/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/audio/src/index.ts) | 2 |
|
|
21
|
-
| `@stacksjs/auth` | [`stacks-auth`](../stacks-auth/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/index.ts) |
|
|
21
|
+
| `@stacksjs/auth` | [`stacks-auth`](../stacks-auth/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/index.ts) | 120 |
|
|
22
22
|
| `@stacksjs/browser` | [`stacks-browser`](../stacks-browser/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/browser/src/index.ts) | 8 |
|
|
23
23
|
| `@stacksjs/browser-extension` | [`stacks-browser-extension`](../stacks-browser-extension/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/browser-extension/src/index.ts) | 7 |
|
|
24
|
-
| `@stacksjs/buddy` | [`stacks-buddy`](../stacks-buddy/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/index.ts) |
|
|
24
|
+
| `@stacksjs/buddy` | [`stacks-buddy`](../stacks-buddy/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/index.ts) | 164 |
|
|
25
25
|
| `@stacksjs/build` | [`stacks-build`](../stacks-build/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/build/src/index.ts) | 3 |
|
|
26
26
|
| `@stacksjs/cache` | [`stacks-cache`](../stacks-cache/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/cache/src/index.ts) | 14 |
|
|
27
27
|
| `@stacksjs/calendar-api` | [`stacks-calendar`](../stacks-calendar/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/calendar-api/src/index.ts) | 2 |
|
|
@@ -65,7 +65,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
|
|
|
65
65
|
| `@stacksjs/newsletter` | [`stacks-newsletter`](../stacks-newsletter/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/newsletter/src/index.ts) | 7 |
|
|
66
66
|
| `@stacksjs/notifications` | [`stacks-notifications`](../stacks-notifications/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/index.ts) | 16 |
|
|
67
67
|
| `@stacksjs/objects` | [`stacks-objects`](../stacks-objects/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/objects/src/index.ts) | 4 |
|
|
68
|
-
| `@stacksjs/orm` | [`stacks-orm`](../stacks-orm/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/index.ts) |
|
|
68
|
+
| `@stacksjs/orm` | [`stacks-orm`](../stacks-orm/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/index.ts) | 72 |
|
|
69
69
|
| `@stacksjs/pagination` | [`stacks-pagination`](../stacks-pagination/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/pagination/src/index.ts) | 0 |
|
|
70
70
|
| `@stacksjs/path` | [`stacks-path`](../stacks-path/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/path/src/index.ts) | 13 |
|
|
71
71
|
| `@stacksjs/payments` | [`stacks-payments`](../stacks-payments/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/payments/src/index.ts) | 28 |
|
|
@@ -79,7 +79,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
|
|
|
79
79
|
| `@stacksjs/scheduler` | [`stacks-scheduler`](../stacks-scheduler/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/scheduler/src/index.ts) | 14 |
|
|
80
80
|
| `@stacksjs/search-engine` | [`stacks-search-engine`](../stacks-search-engine/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/search-engine/src/index.ts) | 10 |
|
|
81
81
|
| `@stacksjs/security` | [`stacks-security`](../stacks-security/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/security/src/index.ts) | 11 |
|
|
82
|
-
| `@stacksjs/server` | [`stacks-server`](../stacks-server/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/index.ts) |
|
|
82
|
+
| `@stacksjs/server` | [`stacks-server`](../stacks-server/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/index.ts) | 26 |
|
|
83
83
|
| `@stacksjs/shell` | [`stacks-shell`](../stacks-shell/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/shell/src/index.ts) | 1 |
|
|
84
84
|
| `@stacksjs/sites` | [`stacks-sites`](../stacks-sites/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/sites/src/index.ts) | 3 |
|
|
85
85
|
| `@stacksjs/skills` | [`stacks-skills`](../stacks-skills/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/skills/src/index.ts) | 1 |
|
|
@@ -185,7 +185,7 @@ Declared runtime exports: `DEFAULT_ROLE_PACKS`, `createBqbRbacStore`, `generateT
|
|
|
185
185
|
|
|
186
186
|
Local source modules: [src/authentication.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/authentication.ts), [src/authenticator.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/authenticator.ts), [src/authorizable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/authorizable.ts), [src/browser-session.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/browser-session.ts), [src/client.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/client.ts), [src/cookie-auth.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/cookie-auth.ts), [src/credential-user.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/credential-user.ts), [src/email-verification.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/email-verification.ts), [src/gate.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/gate.ts), [src/magic-link.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/magic-link.ts), [src/middleware.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/middleware.ts), [src/oauth-authorization-codes.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-codes.ts), [src/oauth-authorization-endpoint.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-endpoint.ts), [src/oauth-authorization-requests.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-requests.ts), [src/oauth-authorization-response.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-response.ts), [src/oauth-authorization.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization.ts), [src/oauth-browser-session.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-browser-session.ts), [src/oauth-client-registration.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-client-registration.ts), [src/oauth-consent.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-consent.ts), [src/oauth-delegated-access.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-delegated-access.ts), [src/oauth-dynamic-registration.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-dynamic-registration.ts), [src/oauth-grants.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-grants.ts), [src/oauth-introspection.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-introspection.ts), [src/oauth-metadata.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-metadata.ts), [src/oauth-pkce.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-pkce.ts), [src/oauth-protected-resource.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-protected-resource.ts), [src/oauth-provider.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-provider.ts), [src/oauth-pruning.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-pruning.ts), [src/oauth-revocation.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-revocation.ts), [src/oauth-server.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-server.ts), [src/oauth-token-endpoint.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-endpoint.ts), [src/oauth-token-exchange.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-exchange.ts), [src/oauth-token-request.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-request.ts), [src/oauth-token-response.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-response.ts), [src/page-gate.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/page-gate.ts), [src/passkey.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/passkey.ts), [src/passkeys.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/passkeys.ts), [src/password/reset.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/password/reset.ts), [src/permissions.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/permissions.ts), [src/policy.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/policy.ts), [src/rate-limiter.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rate-limiter.ts), [src/rbac-seed.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rbac-seed.ts), [src/rbac-store-bqb.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rbac-store-bqb.ts), [src/rbac.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rbac.ts), [src/referrals.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/referrals.ts), [src/register.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/register.ts), [src/request-token.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/request-token.ts), [src/session-auth.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/session-auth.ts), [src/session-handoff.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/session-handoff.ts), [src/socials.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/socials.ts), [src/team.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/team.ts), [src/tokens.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/tokens.ts), [src/two-factor.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/two-factor.ts), [src/user.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/user.ts).
|
|
187
187
|
|
|
188
|
-
Evidence directory: [
|
|
188
|
+
Evidence directory: [120 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
|
|
189
189
|
|
|
190
190
|
### @stacksjs/browser
|
|
191
191
|
|
|
@@ -215,7 +215,7 @@ Read the entrypoint and package export map for re-exported or type-only surfaces
|
|
|
215
215
|
|
|
216
216
|
Local source modules: [src/commands/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/commands/index.ts).
|
|
217
217
|
|
|
218
|
-
Evidence directory: [
|
|
218
|
+
Evidence directory: [164 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
|
|
219
219
|
|
|
220
220
|
### @stacksjs/build
|
|
221
221
|
|
|
@@ -633,7 +633,7 @@ Declared runtime exports: `Activity`, `AnalyticsEvent`, `Auction`, `AuctionItem`
|
|
|
633
633
|
|
|
634
634
|
Local source modules: [src/auto-crud.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/auto-crud.ts), [src/batch-loader.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/batch-loader.ts), [src/db.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/db.ts), [src/define-model.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/define-model.ts), [src/extend-model.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/extend-model.ts), [src/gdpr/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/gdpr/index.ts), [src/generate-database-schema.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/generate-database-schema.ts), [src/locked-row.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/locked-row.ts), [src/messaging.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/messaging.ts), [src/model-types.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/model-types.ts), [src/ownership.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/ownership.ts), [src/paginator-request.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/paginator-request.ts), [src/row-token.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/row-token.ts), [src/subquery.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/subquery.ts), [src/traits/audit.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/audit.ts), [src/traits/billable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/billable.ts), [src/traits/categorizable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/categorizable.ts), [src/traits/commentable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/commentable.ts), [src/traits/likeable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/likeable.ts), [src/traits/taggable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/taggable.ts), [src/transaction.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/transaction.ts), [src/types.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/types.ts), [src/utils/encrypted.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/utils/encrypted.ts), [src/utils/prunable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/utils/prunable.ts).
|
|
635
635
|
|
|
636
|
-
Evidence directory: [
|
|
636
|
+
Evidence directory: [72 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
|
|
637
637
|
|
|
638
638
|
### @stacksjs/pagination
|
|
639
639
|
|
|
@@ -763,7 +763,7 @@ Declared runtime exports: `server`.
|
|
|
763
763
|
|
|
764
764
|
Local source modules: [src/config.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/config.ts), [src/controllers/base.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/controllers/base.ts), [src/imports.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/imports.ts), [src/maintenance.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/maintenance.ts), [src/proxy.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/proxy.ts), [src/redirects.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/redirects.ts), [src/rewrites.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/rewrites.ts), [src/security-headers.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/security-headers.ts), [src/shadowed-defaults.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/shadowed-defaults.ts).
|
|
765
765
|
|
|
766
|
-
Evidence directory: [
|
|
766
|
+
Evidence directory: [26 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
|
|
767
767
|
|
|
768
768
|
### @stacksjs/shell
|
|
769
769
|
|
package/ide/vscode/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"publisher": "Stacks",
|
|
3
3
|
"name": "vscode-stacks",
|
|
4
4
|
"displayName": "Stacks",
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.267",
|
|
6
6
|
"description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"funding": "https://github.com/sponsors/chrisbbreuer",
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/defaults",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.267",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/stacksjs/stacks.git",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@iconify-json/f7": "^1.2.2",
|
|
47
47
|
"@iconify-json/hugeicons": "^1.2.35",
|
|
48
|
-
"@stacksjs/mobile": "0.75.
|
|
49
|
-
"@stacksjs/sanitizer": "0.2.
|
|
50
|
-
"@stacksjs/stx": "0.2.
|
|
48
|
+
"@stacksjs/mobile": "0.75.267",
|
|
49
|
+
"@stacksjs/sanitizer": "0.2.664",
|
|
50
|
+
"@stacksjs/stx": "0.2.664",
|
|
51
51
|
"@xterm/addon-fit": "^0.11.0",
|
|
52
52
|
"@xterm/xterm": "^6.0.0",
|
|
53
53
|
"ts-qr-codes": "^0.1.8"
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import { dashboardApi } from '../../../../functions/dashboard-api'
|
|
3
5
|
|
|
@@ -67,29 +69,75 @@ const filteredRequests = derived(() => {
|
|
|
67
69
|
})
|
|
68
70
|
})
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
function statusClass(status: number): string {
|
|
72
|
+
/**
|
|
73
|
+
* Pill colours by tone rather than by value, so every status code and every
|
|
74
|
+
* method the table has not named still lands on a colour, as the hand-rolled
|
|
75
|
+
* pills did.
|
|
76
|
+
*/
|
|
77
|
+
function statusTone(status: number): string {
|
|
78
78
|
if (status >= 500)
|
|
79
|
-
return '
|
|
79
|
+
return 'server'
|
|
80
80
|
if (status >= 400)
|
|
81
|
-
return '
|
|
81
|
+
return 'client'
|
|
82
82
|
if (status >= 300)
|
|
83
|
-
return '
|
|
84
|
-
return '
|
|
83
|
+
return 'redirect'
|
|
84
|
+
return 'ok'
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
function
|
|
87
|
+
function methodTone(method: string): string {
|
|
88
88
|
if (method === 'GET')
|
|
89
|
-
return '
|
|
89
|
+
return 'read'
|
|
90
90
|
if (method === 'DELETE')
|
|
91
|
-
return '
|
|
92
|
-
return '
|
|
91
|
+
return 'delete'
|
|
92
|
+
return 'write'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const COLUMNS = [
|
|
96
|
+
{
|
|
97
|
+
key: 'methodTone',
|
|
98
|
+
label: 'Method',
|
|
99
|
+
type: 'badge' as const,
|
|
100
|
+
sortable: true,
|
|
101
|
+
badgeLabelKey: 'method',
|
|
102
|
+
badgeClasses: {
|
|
103
|
+
read: 'text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40',
|
|
104
|
+
delete: 'text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-950/40',
|
|
105
|
+
write: 'text-violet-700 dark:text-violet-300 bg-violet-50 dark:bg-violet-950/40',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{ key: 'path', label: 'Path', sortable: true, class: 'max-w-md font-mono text-neutral-800 text-xs truncate dark:text-neutral-200' },
|
|
109
|
+
{
|
|
110
|
+
key: 'statusTone',
|
|
111
|
+
label: 'Status',
|
|
112
|
+
type: 'badge' as const,
|
|
113
|
+
sortable: true,
|
|
114
|
+
badgeLabelKey: 'status',
|
|
115
|
+
badgeClasses: {
|
|
116
|
+
server: 'text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-950/40',
|
|
117
|
+
client: 'text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-950/40',
|
|
118
|
+
redirect: 'text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40',
|
|
119
|
+
ok: 'text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/40',
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{ key: 'durationMs', label: 'Duration (ms)', type: 'number' as const, sortable: true, align: 'right' as const, class: 'font-mono whitespace-nowrap' },
|
|
123
|
+
{ key: 'ipAddress', label: 'Client', sortable: true, emptyText: 'Not recorded' },
|
|
124
|
+
{ key: 'createdAt', label: 'Recorded', type: 'datetime' as const, sortable: true, align: 'right' as const, emptyText: 'Unknown', class: 'whitespace-nowrap' },
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
const ROW_ACTIONS = [
|
|
128
|
+
{ action: 'inspect', label: 'Inspect request', icon: 'i-hugeicons-view' },
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
const requestRows = derived(() => filteredRequests().map(request => ({
|
|
132
|
+
...request,
|
|
133
|
+
methodTone: methodTone(request.method),
|
|
134
|
+
statusTone: statusTone(request.status),
|
|
135
|
+
durationMs: Math.round(request.durationMs * 10) / 10,
|
|
136
|
+
})))
|
|
137
|
+
|
|
138
|
+
function handleRowAction(payload: { action: string, row: RequestRow }): void {
|
|
139
|
+
if (payload.action === 'inspect')
|
|
140
|
+
inspectRequest(payload.row)
|
|
93
141
|
}
|
|
94
142
|
|
|
95
143
|
function inspectRequest(request: RequestRow): void {
|
|
@@ -184,49 +232,18 @@ onMount(() => {
|
|
|
184
232
|
</div>
|
|
185
233
|
</div>
|
|
186
234
|
|
|
187
|
-
<
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
:title="search() || methodFilter() !== 'all' || statusFilter() !== 'all' ? 'No matching requests' : 'No requests recorded'"
|
|
196
|
-
description="Request records appear after the application writes to the Request model."
|
|
235
|
+
<Table
|
|
236
|
+
:columns="COLUMNS"
|
|
237
|
+
:data="requestRows()"
|
|
238
|
+
:loading="loading()"
|
|
239
|
+
:row-actions="ROW_ACTIONS"
|
|
240
|
+
action-label-key="path"
|
|
241
|
+
paginated
|
|
242
|
+
:page-size="25"
|
|
243
|
+
:empty-title="search() || methodFilter() !== 'all' || statusFilter() !== 'all' ? 'No matching requests' : 'No requests recorded'"
|
|
244
|
+
empty-description="Request records appear after the application writes to the Request model."
|
|
245
|
+
@row-action="handleRowAction"
|
|
197
246
|
/>
|
|
198
|
-
|
|
199
|
-
<div :if="!loading() && filteredRequests().length > 0" class="overflow-x-auto">
|
|
200
|
-
<table class="w-full">
|
|
201
|
-
<thead class="bg-neutral-50 dark:bg-neutral-800/50">
|
|
202
|
-
<tr>
|
|
203
|
-
<th class="pl-5 pr-4 py-3 font-semibold text-left text-neutral-500 text-xs tracking-wider uppercase dark:text-neutral-400">Request</th>
|
|
204
|
-
<th class="px-4 py-3 font-semibold text-left text-neutral-500 text-xs tracking-wider uppercase dark:text-neutral-400">Status</th>
|
|
205
|
-
<th class="px-4 py-3 font-semibold text-neutral-500 text-right text-xs tracking-wider uppercase dark:text-neutral-400">Duration</th>
|
|
206
|
-
<th class="px-4 py-3 font-semibold text-left text-neutral-500 text-xs tracking-wider uppercase dark:text-neutral-400">Client</th>
|
|
207
|
-
<th class="px-4 py-3 font-semibold text-neutral-500 text-right text-xs tracking-wider uppercase dark:text-neutral-400">Recorded</th>
|
|
208
|
-
<th class="pr-5 py-3 font-semibold text-neutral-500 text-right text-xs tracking-wider uppercase dark:text-neutral-400">Details</th>
|
|
209
|
-
</tr>
|
|
210
|
-
</thead>
|
|
211
|
-
<tbody class="divide-neutral-100 divide-y dark:divide-neutral-700">
|
|
212
|
-
<tr :for="request in filteredRequests()">
|
|
213
|
-
<td class="pl-5 pr-4 py-3.5">
|
|
214
|
-
<div class="flex gap-2 items-center">
|
|
215
|
-
<span :class="'inline-flex px-2 py-0.5 font-medium text-xs rounded ' + methodClass(request.method)">{{ request.method }}</span>
|
|
216
|
-
<code class="max-w-md text-neutral-800 text-xs truncate dark:text-neutral-200">{{ request.path }}</code>
|
|
217
|
-
</div>
|
|
218
|
-
</td>
|
|
219
|
-
<td class="px-4 py-3.5"><span :class="'inline-flex px-2 py-0.5 font-medium text-xs rounded ' + statusClass(request.status)">{{ request.status }}</span></td>
|
|
220
|
-
<td class="px-4 py-3.5 font-mono text-neutral-600 text-right text-xs whitespace-nowrap dark:text-neutral-300">{{ request.durationMs.toFixed(1) }} ms</td>
|
|
221
|
-
<td class="px-4 py-3.5 text-neutral-500 text-xs dark:text-neutral-400">{{ request.ipAddress || 'Not recorded' }}</td>
|
|
222
|
-
<td class="px-4 py-3.5 text-neutral-500 text-right text-xs whitespace-nowrap dark:text-neutral-400">{{ formatDate(request.createdAt) }}</td>
|
|
223
|
-
<td class="pr-5 py-3.5 text-right">
|
|
224
|
-
<Button variant="ghost" size="sm" @click="inspectRequest(request)">Inspect</Button>
|
|
225
|
-
</td>
|
|
226
|
-
</tr>
|
|
227
|
-
</tbody>
|
|
228
|
-
</table>
|
|
229
|
-
</div>
|
|
230
247
|
</Card>
|
|
231
248
|
|
|
232
249
|
<RequestDetailsDialog :request="selectedRequest()" @close="closeInspector" />
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import { dashboardApi } from '../../../../functions/dashboard-api'
|
|
3
5
|
|
|
@@ -44,9 +46,7 @@ const page = derived(() => mode() === 'actions'
|
|
|
44
46
|
empty: 'No Action modules were found.',
|
|
45
47
|
scaffold: 'buddy make:action MyAction',
|
|
46
48
|
icon: 'i-hugeicons-energy',
|
|
47
|
-
detailLabel: 'Method',
|
|
48
49
|
note: 'Application files override matching framework defaults.',
|
|
49
|
-
showOrigin: true,
|
|
50
50
|
stats: [
|
|
51
51
|
{ key: 'total', label: 'Total Actions' },
|
|
52
52
|
{ key: 'application', label: 'Application Overrides' },
|
|
@@ -60,9 +60,7 @@ const page = derived(() => mode() === 'actions'
|
|
|
60
60
|
empty: 'No command files in app/Commands.',
|
|
61
61
|
scaffold: 'buddy make:command my-command',
|
|
62
62
|
icon: 'i-hugeicons-command-line',
|
|
63
|
-
detailLabel: 'Aliases and options',
|
|
64
63
|
note: 'Every file in app/Commands is a command. The optional app/Commands.ts only orders, aliases or disables them.',
|
|
65
|
-
showOrigin: false,
|
|
66
64
|
stats: [
|
|
67
65
|
{ key: 'total', label: 'Commands' },
|
|
68
66
|
{ key: 'registered', label: 'Registry Entries' },
|
|
@@ -90,13 +88,6 @@ function statValue(key: string): number {
|
|
|
90
88
|
return Number(stats()[key] || 0)
|
|
91
89
|
}
|
|
92
90
|
|
|
93
|
-
function formatDate(value: string): string {
|
|
94
|
-
if (!value)
|
|
95
|
-
return 'Unknown'
|
|
96
|
-
const date = new Date(value)
|
|
97
|
-
return Number.isNaN(date.getTime()) ? 'Unknown' : date.toLocaleString()
|
|
98
|
-
}
|
|
99
|
-
|
|
100
91
|
function formatAliases(item: SourceItem): string {
|
|
101
92
|
return item.aliases?.length ? item.aliases.join(', ') : 'None'
|
|
102
93
|
}
|
|
@@ -105,14 +96,67 @@ function formatOptions(item: SourceItem): string {
|
|
|
105
96
|
return item.options?.length ? item.options.join(', ') : 'None'
|
|
106
97
|
}
|
|
107
98
|
|
|
108
|
-
|
|
99
|
+
/** Read-only methods, writes, and the catch-all, as the hand-rolled pill coloured them. */
|
|
100
|
+
function methodTone(method = 'ANY'): string {
|
|
109
101
|
if (['GET', 'HEAD', 'OPTIONS'].includes(method))
|
|
110
|
-
return '
|
|
102
|
+
return 'read'
|
|
111
103
|
if (method === 'ANY')
|
|
112
|
-
return '
|
|
113
|
-
return '
|
|
104
|
+
return 'any'
|
|
105
|
+
return 'write'
|
|
114
106
|
}
|
|
115
107
|
|
|
108
|
+
const NAME_COLUMN = { key: 'name', label: 'Name', sortable: true, subtitleKey: 'description', class: 'font-medium text-neutral-900 dark:text-white' }
|
|
109
|
+
const PATH_COLUMN = { key: 'path', label: 'Path', sortable: true, class: 'font-mono text-neutral-600 text-xs dark:text-neutral-300' }
|
|
110
|
+
const MODIFIED_COLUMN = { key: 'modifiedAt', label: 'Modified', type: 'datetime' as const, sortable: true, align: 'right' as const, emptyText: 'Unknown', class: 'whitespace-nowrap' }
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Actions show a method and where the file comes from. Commands have one
|
|
114
|
+
* root, so their second column is what can be typed instead.
|
|
115
|
+
*/
|
|
116
|
+
const COLUMNS_BY_MODE = {
|
|
117
|
+
actions: [
|
|
118
|
+
NAME_COLUMN,
|
|
119
|
+
{
|
|
120
|
+
key: 'methodTone',
|
|
121
|
+
label: 'Method',
|
|
122
|
+
type: 'badge' as const,
|
|
123
|
+
sortable: true,
|
|
124
|
+
badgeLabelKey: 'methodLabel',
|
|
125
|
+
badgeClasses: {
|
|
126
|
+
read: 'text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40',
|
|
127
|
+
any: 'text-neutral-700 dark:text-neutral-300 bg-neutral-100 dark:bg-neutral-800',
|
|
128
|
+
write: 'text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-950/40',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
key: 'origin',
|
|
133
|
+
label: 'Origin',
|
|
134
|
+
type: 'badge' as const,
|
|
135
|
+
sortable: true,
|
|
136
|
+
badgeClasses: {
|
|
137
|
+
Application: 'text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/40',
|
|
138
|
+
Framework: 'text-neutral-700 dark:text-neutral-300 bg-neutral-100 dark:bg-neutral-800',
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
PATH_COLUMN,
|
|
142
|
+
MODIFIED_COLUMN,
|
|
143
|
+
],
|
|
144
|
+
commands: [
|
|
145
|
+
NAME_COLUMN,
|
|
146
|
+
{ key: 'aliasesLabel', label: 'Aliases', sortable: true, subtitleKey: 'optionsLabel', class: 'text-neutral-700 text-xs dark:text-neutral-300' },
|
|
147
|
+
PATH_COLUMN,
|
|
148
|
+
MODIFIED_COLUMN,
|
|
149
|
+
],
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const itemRows = derived(() => filteredItems().map(item => ({
|
|
153
|
+
...item,
|
|
154
|
+
methodTone: methodTone(item.method),
|
|
155
|
+
methodLabel: item.method || 'ANY',
|
|
156
|
+
aliasesLabel: `Aliases: ${formatAliases(item)}`,
|
|
157
|
+
optionsLabel: `Options: ${formatOptions(item)}`,
|
|
158
|
+
})))
|
|
159
|
+
|
|
116
160
|
async function loadInventory(): Promise<void> {
|
|
117
161
|
loading.set(true)
|
|
118
162
|
loadError.set('')
|
|
@@ -205,50 +249,15 @@ onMount(() => {
|
|
|
205
249
|
</label>
|
|
206
250
|
</div>
|
|
207
251
|
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
:
|
|
215
|
-
:
|
|
216
|
-
:
|
|
217
|
-
:description="search() ? 'Try a different name, path, method, or alias.' : 'Use the scaffold command to add one through the native framework workflow.'"
|
|
252
|
+
<Table
|
|
253
|
+
:columns="COLUMNS_BY_MODE[mode()]"
|
|
254
|
+
:data="itemRows()"
|
|
255
|
+
:loading="loading()"
|
|
256
|
+
row-key="path"
|
|
257
|
+
paginated
|
|
258
|
+
:page-size="25"
|
|
259
|
+
:empty-title="search() ? 'No matching source files' : page().empty"
|
|
260
|
+
:empty-description="search() ? 'Try a different name, path, method, or alias.' : 'Use the scaffold command to add one through the native framework workflow.'"
|
|
218
261
|
/>
|
|
219
|
-
|
|
220
|
-
<div :if="!loading() && filteredItems().length > 0" class="overflow-x-auto">
|
|
221
|
-
<table class="w-full">
|
|
222
|
-
<thead class="bg-neutral-50 dark:bg-neutral-800/50">
|
|
223
|
-
<tr>
|
|
224
|
-
<th class="pl-5 pr-4 py-3 font-semibold text-left text-neutral-500 text-xs tracking-wider uppercase dark:text-neutral-400">Name</th>
|
|
225
|
-
<th class="px-4 py-3 font-semibold text-left text-neutral-500 text-xs tracking-wider uppercase dark:text-neutral-400">{{ page().detailLabel }}</th>
|
|
226
|
-
<th :if="page().showOrigin" class="px-4 py-3 font-semibold text-left text-neutral-500 text-xs tracking-wider uppercase dark:text-neutral-400">Origin</th>
|
|
227
|
-
<th class="px-4 py-3 font-semibold text-left text-neutral-500 text-xs tracking-wider uppercase dark:text-neutral-400">Path</th>
|
|
228
|
-
<th class="pr-5 py-3 font-semibold text-neutral-500 text-right text-xs tracking-wider uppercase dark:text-neutral-400">Modified</th>
|
|
229
|
-
</tr>
|
|
230
|
-
</thead>
|
|
231
|
-
<tbody class="divide-neutral-100 divide-y dark:divide-neutral-700">
|
|
232
|
-
<tr :for="item in filteredItems()">
|
|
233
|
-
<td class="pl-5 pr-4 py-3.5">
|
|
234
|
-
<p class="font-medium text-neutral-900 text-sm dark:text-white">{{ item.name }}</p>
|
|
235
|
-
<p class="mt-0.5 max-w-md text-neutral-500 text-xs dark:text-neutral-400">{{ item.description }}</p>
|
|
236
|
-
</td>
|
|
237
|
-
<td class="px-4 py-3.5">
|
|
238
|
-
<span :if="mode() === 'actions'" :class="'inline-flex px-2 py-0.5 font-medium text-xs rounded ' + methodClass(item.method)">{{ item.method || 'ANY' }}</span>
|
|
239
|
-
<div :if="mode() === 'commands'" class="space-y-1 text-xs">
|
|
240
|
-
<p class="text-neutral-700 dark:text-neutral-300"><span class="text-neutral-500 dark:text-neutral-400">Aliases:</span> {{ formatAliases(item) }}</p>
|
|
241
|
-
<p class="text-neutral-700 dark:text-neutral-300"><span class="text-neutral-500 dark:text-neutral-400">Options:</span> {{ formatOptions(item) }}</p>
|
|
242
|
-
</div>
|
|
243
|
-
</td>
|
|
244
|
-
<td :if="page().showOrigin" class="px-4 py-3.5">
|
|
245
|
-
<span :class="'inline-flex px-2 py-0.5 font-medium text-xs rounded ' + (item.origin === 'Application' ? 'text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/40' : 'text-neutral-700 dark:text-neutral-300 bg-neutral-100 dark:bg-neutral-800')">{{ item.origin }}</span>
|
|
246
|
-
</td>
|
|
247
|
-
<td class="px-4 py-3.5 font-mono text-neutral-600 text-xs dark:text-neutral-300">{{ item.path }}</td>
|
|
248
|
-
<td class="pr-5 py-3.5 text-neutral-500 text-right text-xs whitespace-nowrap dark:text-neutral-400">{{ formatDate(item.modifiedAt) }}</td>
|
|
249
|
-
</tr>
|
|
250
|
-
</tbody>
|
|
251
|
-
</table>
|
|
252
|
-
</div>
|
|
253
262
|
</Card>
|
|
254
263
|
</div>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import type {
|
|
3
5
|
DashboardDnsDomain,
|
|
@@ -61,10 +63,40 @@ function typeColor(type: string): string {
|
|
|
61
63
|
return 'bg-rose-500'
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
const TYPE_BADGE = 'font-mono font-semibold text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40'
|
|
67
|
+
|
|
68
|
+
const COLUMNS = [
|
|
69
|
+
{
|
|
70
|
+
key: 'type',
|
|
71
|
+
label: 'Type',
|
|
72
|
+
type: 'badge' as const,
|
|
73
|
+
sortable: true,
|
|
74
|
+
badgeClasses: { A: TYPE_BADGE, AAAA: TYPE_BADGE, CNAME: TYPE_BADGE, MX: TYPE_BADGE, TXT: TYPE_BADGE },
|
|
75
|
+
},
|
|
76
|
+
{ key: 'name', label: 'Name', sortable: true, class: 'font-mono text-neutral-900 whitespace-nowrap dark:text-white' },
|
|
77
|
+
{ key: 'value', label: 'Value', sortable: true, emptyText: 'Not set', class: 'max-w-md font-mono truncate' },
|
|
78
|
+
{ key: 'ttlLabel', label: 'TTL', class: 'whitespace-nowrap' },
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
const ROW_ACTIONS = [
|
|
82
|
+
{ action: 'inspect', label: 'Inspect', icon: 'i-hugeicons-view' },
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
const recordRows = derived(() => filteredRecords().map((record, index) => ({
|
|
86
|
+
...record,
|
|
87
|
+
rowId: `${record.type}:${record.name}:${index}`,
|
|
88
|
+
ttlLabel: ttlLabel(record),
|
|
89
|
+
})))
|
|
90
|
+
|
|
64
91
|
function inspectRecord(record: DashboardDnsRecord): void {
|
|
65
92
|
selectedRecord.set(record)
|
|
66
93
|
}
|
|
67
94
|
|
|
95
|
+
function handleRowAction(payload: { action: string, row: DashboardDnsRecord }): void {
|
|
96
|
+
if (payload.action === 'inspect')
|
|
97
|
+
inspectRecord(payload.row)
|
|
98
|
+
}
|
|
99
|
+
|
|
68
100
|
function closeRecord(): void {
|
|
69
101
|
selectedRecord.set(null)
|
|
70
102
|
}
|
|
@@ -202,34 +234,20 @@ onMount(() => {
|
|
|
202
234
|
</div>
|
|
203
235
|
</div>
|
|
204
236
|
|
|
205
|
-
<div
|
|
206
|
-
<
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
<th scope="col" class="px-5 py-3 font-semibold text-left text-neutral-500 text-xs uppercase dark:text-neutral-400">TTL</th>
|
|
220
|
-
<th scope="col" class="px-5 py-3"><span class="sr-only">Actions</span></th>
|
|
221
|
-
</tr>
|
|
222
|
-
</thead>
|
|
223
|
-
<tbody class="divide-neutral-100 divide-y dark:divide-neutral-800">
|
|
224
|
-
<tr :for="record in filteredRecords()" class="hover:bg-neutral-50 dark:hover:bg-neutral-800/50">
|
|
225
|
-
<td class="px-5 py-4"><span class="inline-flex px-2 py-1 font-mono font-semibold text-blue-700 text-xs dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40 rounded-md">{{ record.type }}</span></td>
|
|
226
|
-
<td class="px-5 py-4 font-mono text-neutral-900 text-sm whitespace-nowrap dark:text-white">{{ record.name }}</td>
|
|
227
|
-
<td class="px-5 py-4 max-w-md font-mono text-neutral-500 text-sm truncate dark:text-neutral-400">{{ record.value || 'Not set' }}</td>
|
|
228
|
-
<td class="px-5 py-4 text-neutral-500 text-sm whitespace-nowrap dark:text-neutral-400">{{ ttlLabel(record) }}</td>
|
|
229
|
-
<td class="px-5 py-4 text-right"><Button variant="ghost" size="sm" @click="inspectRecord(record)">Inspect</Button></td>
|
|
230
|
-
</tr>
|
|
231
|
-
</tbody>
|
|
232
|
-
</table>
|
|
237
|
+
<div class="mt-4 bg-white dark:bg-neutral-900 border border-neutral-200 rounded-xl dark:border-neutral-700">
|
|
238
|
+
<Table
|
|
239
|
+
:columns="COLUMNS"
|
|
240
|
+
:data="recordRows()"
|
|
241
|
+
:loading="loading()"
|
|
242
|
+
:row-actions="ROW_ACTIONS"
|
|
243
|
+
action-label-key="name"
|
|
244
|
+
row-key="rowId"
|
|
245
|
+
paginated
|
|
246
|
+
:page-size="25"
|
|
247
|
+
:empty-title="records().length ? 'No matching DNS records' : 'No DNS records configured'"
|
|
248
|
+
:empty-description="records().length ? 'Adjust the search or record type filter.' : 'Add records to config/dns.ts, then refresh this page.'"
|
|
249
|
+
@row-action="handleRowAction"
|
|
250
|
+
/>
|
|
233
251
|
</div>
|
|
234
252
|
</section>
|
|
235
253
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import type { ReleaseRecord, ReleaseSummary } from '../../../../app/Actions/Dashboard/Releases/release-records'
|
|
3
5
|
import { dashboardApi } from '../../../../functions/dashboard-api'
|
|
@@ -41,27 +43,30 @@ function formatNumber(value: number): string {
|
|
|
41
43
|
return new Intl.NumberFormat().format(value)
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
const COLUMNS = [
|
|
47
|
+
{ key: 'version', label: 'Version', sortable: true, subtitleKey: 'notes', class: 'font-semibold text-gray-900 dark:text-white' },
|
|
48
|
+
{ key: 'type', label: 'Type', sortable: true, class: 'capitalize' },
|
|
49
|
+
{
|
|
50
|
+
key: 'statusKey',
|
|
51
|
+
label: 'Status',
|
|
52
|
+
type: 'badge' as const,
|
|
53
|
+
sortable: true,
|
|
54
|
+
badgeLabelKey: 'status',
|
|
55
|
+
badgeClasses: {
|
|
56
|
+
published: 'capitalize text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/30',
|
|
57
|
+
scheduled: 'capitalize text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/30',
|
|
58
|
+
draft: 'capitalize text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-950/30',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{ key: 'downloads', label: 'Downloads', type: 'number' as const, sortable: true, align: 'right' as const, class: 'font-mono' },
|
|
62
|
+
{ key: 'author', label: 'Author', sortable: true, emptyText: 'Not recorded' },
|
|
63
|
+
{ key: 'createdAt', label: 'Created', type: 'datetime' as const, sortable: true, emptyText: 'Not recorded', class: 'whitespace-nowrap' },
|
|
64
|
+
]
|
|
55
65
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return 'text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/30'
|
|
61
|
-
if (value === 'draft')
|
|
62
|
-
return 'text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-950/30'
|
|
63
|
-
return 'text-gray-700 dark:text-neutral-300 bg-gray-100 dark:bg-neutral-700'
|
|
64
|
-
}
|
|
66
|
+
const releaseRows = derived(() => rows().map(release => ({
|
|
67
|
+
...release,
|
|
68
|
+
statusKey: release.status.toLowerCase(),
|
|
69
|
+
})))
|
|
65
70
|
|
|
66
71
|
async function loadReleases(): Promise<void> {
|
|
67
72
|
isLoading.set(true)
|
|
@@ -141,47 +146,16 @@ onMount(() => {
|
|
|
141
146
|
</div>
|
|
142
147
|
</div>
|
|
143
148
|
|
|
144
|
-
<div class="
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
<div :else class="overflow-x-auto">
|
|
155
|
-
<table class="min-w-full divide-gray-200 divide-y dark:divide-neutral-700">
|
|
156
|
-
<thead class="bg-gray-50 dark:bg-neutral-900/50">
|
|
157
|
-
<tr>
|
|
158
|
-
<th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs uppercase dark:text-neutral-400">Version</th>
|
|
159
|
-
<th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs uppercase dark:text-neutral-400">Type</th>
|
|
160
|
-
<th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs uppercase dark:text-neutral-400">Status</th>
|
|
161
|
-
<th class="px-4 py-3 font-semibold text-gray-600 text-right text-xs uppercase dark:text-neutral-400">Downloads</th>
|
|
162
|
-
<th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs uppercase dark:text-neutral-400">Author</th>
|
|
163
|
-
<th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs uppercase dark:text-neutral-400">Created</th>
|
|
164
|
-
</tr>
|
|
165
|
-
</thead>
|
|
166
|
-
<tbody class="divide-gray-100 divide-y dark:divide-neutral-700">
|
|
167
|
-
<template :for="release in rows()" :key="release.id">
|
|
168
|
-
<tr>
|
|
169
|
-
<td class="px-4 py-3">
|
|
170
|
-
<p class="font-semibold text-gray-900 text-sm dark:text-white">{{ release.version }}</p>
|
|
171
|
-
<p :if="release.notes" class="mt-1 max-w-md text-gray-500 text-xs truncate dark:text-neutral-400">{{ release.notes }}</p>
|
|
172
|
-
</td>
|
|
173
|
-
<td class="px-4 py-3 capitalize text-gray-600 text-sm dark:text-neutral-300">{{ release.type }}</td>
|
|
174
|
-
<td class="px-4 py-3">
|
|
175
|
-
<span :class="'inline-flex px-2 py-1 font-medium text-xs capitalize rounded-full ' + statusClass(release.status)">{{ release.status }}</span>
|
|
176
|
-
</td>
|
|
177
|
-
<td class="px-4 py-3 font-mono text-gray-700 text-right text-sm dark:text-neutral-200">{{ formatNumber(release.downloads) }}</td>
|
|
178
|
-
<td class="px-4 py-3 text-gray-600 text-sm dark:text-neutral-300">{{ release.author || 'Not recorded' }}</td>
|
|
179
|
-
<td class="px-4 py-3 text-gray-600 text-sm whitespace-nowrap dark:text-neutral-300">{{ formatDate(release.createdAt) }}</td>
|
|
180
|
-
</tr>
|
|
181
|
-
</template>
|
|
182
|
-
</tbody>
|
|
183
|
-
</table>
|
|
184
|
-
</div>
|
|
149
|
+
<div class="bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
|
|
150
|
+
<Table
|
|
151
|
+
:columns="COLUMNS"
|
|
152
|
+
:data="releaseRows()"
|
|
153
|
+
:loading="isLoading()"
|
|
154
|
+
paginated
|
|
155
|
+
:page-size="25"
|
|
156
|
+
empty-title="No release records found"
|
|
157
|
+
empty-description="Create releases through your application workflow and they will appear here."
|
|
158
|
+
/>
|
|
185
159
|
</div>
|
|
186
160
|
</section>
|
|
187
161
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
@import('../UI/Button', '../UI/ConfirmDialog')
|
|
1
|
+
@import('../UI/Button', '../UI/ConfirmDialog', '../UI/Table')
|
|
2
2
|
|
|
3
3
|
<script client>
|
|
4
4
|
import { dashboardApi } from '../../../../functions/dashboard-api'
|
|
@@ -53,8 +53,6 @@ const memberToRemove = state<TeamMember | null>(null)
|
|
|
53
53
|
const invitationToRevoke = state<TeamInvitation | null>(null)
|
|
54
54
|
|
|
55
55
|
const teamId = derived(() => Number(route.params.id))
|
|
56
|
-
const hasMembers = derived(() => members().length > 0)
|
|
57
|
-
const hasInvitations = derived(() => invitations().length > 0)
|
|
58
56
|
|
|
59
57
|
function roleLabel(role: string): string {
|
|
60
58
|
return role.charAt(0).toUpperCase() + role.slice(1)
|
|
@@ -65,24 +63,64 @@ function memberCountLabel(): string {
|
|
|
65
63
|
return `${count} team ${count === 1 ? 'member' : 'members'}`
|
|
66
64
|
}
|
|
67
65
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
/**
|
|
67
|
+
* One palette for member status and invitation delivery, keyed by value, so
|
|
68
|
+
* a status the server adds later still reads, in the neutral pill.
|
|
69
|
+
*/
|
|
70
|
+
const STATUS_CLASSES = {
|
|
71
|
+
active: 'bg-green-50 text-green-700 dark:bg-green-500/10 dark:text-green-400',
|
|
72
|
+
sent: 'bg-green-50 text-green-700 dark:bg-green-500/10 dark:text-green-400',
|
|
73
|
+
suspended: 'bg-red-50 text-red-700 dark:bg-red-500/10 dark:text-red-400',
|
|
74
|
+
failed: 'bg-red-50 text-red-700 dark:bg-red-500/10 dark:text-red-400',
|
|
75
|
+
expired: 'bg-amber-50 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400',
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
78
|
+
/**
|
|
79
|
+
* The member and role columns are slotted: the first for its avatar, the
|
|
80
|
+
* second because an owner's role is fixed and everyone else's is a select.
|
|
81
|
+
*/
|
|
82
|
+
const MEMBER_COLUMNS = [
|
|
83
|
+
{ key: 'name', label: 'Member', sortable: true, slotted: true },
|
|
84
|
+
{ key: 'role', label: 'Role', sortable: true, slotted: true },
|
|
85
|
+
{ key: 'status', label: 'Status', type: 'badge' as const, sortable: true, badgeLabelKey: 'statusLabel', badgeClasses: STATUS_CLASSES },
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* An owner can be neither suspended nor removed. Anyone else is offered the
|
|
90
|
+
* opposite of their current state, and every status but `active` reads as
|
|
91
|
+
* one to activate, as the hand-rolled button did.
|
|
92
|
+
*/
|
|
93
|
+
const MEMBER_ACTIONS = [
|
|
94
|
+
{ action: 'suspend', label: 'Suspend', icon: 'i-hugeicons-pause', class: 'text-amber-500', when: { key: 'statusAction', equals: 'suspend' } },
|
|
95
|
+
{ action: 'activate', label: 'Activate', icon: 'i-hugeicons-play', class: 'text-green-500', when: { key: 'statusAction', equals: 'activate' } },
|
|
96
|
+
{ action: 'remove', label: 'Remove', icon: 'i-hugeicons-delete-02', class: 'text-red-500', when: { key: 'removable', equals: true } },
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
const INVITATION_COLUMNS = [
|
|
100
|
+
{ key: 'email', label: 'Recipient', sortable: true, class: 'text-neutral-900 dark:text-neutral-100' },
|
|
101
|
+
{ key: 'roleLabel', label: 'Role', sortable: true },
|
|
102
|
+
{ key: 'deliveryStatus', label: 'Delivery', type: 'badge' as const, sortable: true, badgeLabelKey: 'deliveryLabel', badgeClasses: STATUS_CLASSES },
|
|
103
|
+
{ key: 'expiresAt', label: 'Expires', type: 'date' as const, sortable: true, emptyText: 'Not available' },
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
const INVITATION_ACTIONS = [
|
|
107
|
+
{ action: 'resend', label: 'Resend invitation to', icon: 'i-hugeicons-mail-send-01' },
|
|
108
|
+
{ action: 'revoke', label: 'Revoke invitation to', icon: 'i-hugeicons-cancel-01', class: 'text-red-500' },
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
const memberRows = derived(() => members().map(member => ({
|
|
112
|
+
...member,
|
|
113
|
+
initial: member.name.charAt(0).toUpperCase(),
|
|
114
|
+
statusLabel: roleLabel(member.status),
|
|
115
|
+
statusAction: member.role === 'owner' ? 'none' : member.status === 'active' ? 'suspend' : 'activate',
|
|
116
|
+
removable: member.role !== 'owner',
|
|
117
|
+
})))
|
|
118
|
+
|
|
119
|
+
const invitationRows = derived(() => invitations().map(invitation => ({
|
|
120
|
+
...invitation,
|
|
121
|
+
roleLabel: roleLabel(invitation.role),
|
|
122
|
+
deliveryLabel: roleLabel(invitation.deliveryStatus),
|
|
123
|
+
})))
|
|
86
124
|
|
|
87
125
|
async function fetchPeople(): Promise<void> {
|
|
88
126
|
isLoading.set(true)
|
|
@@ -233,6 +271,20 @@ async function revokeInvitation(): Promise<void> {
|
|
|
233
271
|
}
|
|
234
272
|
}
|
|
235
273
|
|
|
274
|
+
function handleMemberAction(payload: { action: string, row: TeamMember }): void {
|
|
275
|
+
if (payload.action === 'remove')
|
|
276
|
+
memberToRemove.set(payload.row)
|
|
277
|
+
else
|
|
278
|
+
void toggleMemberStatus(payload.row)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function handleInvitationAction(payload: { action: string, row: TeamInvitation }): void {
|
|
282
|
+
if (payload.action === 'resend')
|
|
283
|
+
void resendInvitation(payload.row)
|
|
284
|
+
else if (payload.action === 'revoke')
|
|
285
|
+
invitationToRevoke.set(payload.row)
|
|
286
|
+
}
|
|
287
|
+
|
|
236
288
|
onMount(() => {
|
|
237
289
|
fetchPeople()
|
|
238
290
|
})
|
|
@@ -307,79 +359,48 @@ onMount(() => {
|
|
|
307
359
|
</div>
|
|
308
360
|
</div>
|
|
309
361
|
|
|
310
|
-
<
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
<td class="px-6 py-4">
|
|
353
|
-
<span class="inline-flex px-2 py-1 font-medium text-xs rounded-full" :class="statusClass(member.status)">
|
|
354
|
-
{{ roleLabel(member.status) }}
|
|
355
|
-
</span>
|
|
356
|
-
</td>
|
|
357
|
-
<td class="px-6 py-4">
|
|
358
|
-
<div :show="member.role !== 'owner'" class="flex gap-2 items-center justify-end">
|
|
359
|
-
<Button
|
|
360
|
-
variant="secondary"
|
|
361
|
-
size="sm"
|
|
362
|
-
:loading="busyMemberId() === member.id"
|
|
363
|
-
:disabled="busyMemberId() === member.id"
|
|
364
|
-
@click="toggleMemberStatus(member)"
|
|
365
|
-
>
|
|
366
|
-
{{ member.status === 'active' ? 'Suspend' : 'Activate' }}
|
|
367
|
-
</Button>
|
|
368
|
-
<Button
|
|
369
|
-
variant="ghost"
|
|
370
|
-
size="sm"
|
|
371
|
-
:disabled="busyMemberId() === member.id"
|
|
372
|
-
@click="memberToRemove.set(member)"
|
|
373
|
-
>
|
|
374
|
-
Remove
|
|
375
|
-
</Button>
|
|
376
|
-
</div>
|
|
377
|
-
</td>
|
|
378
|
-
</tr>
|
|
379
|
-
</template>
|
|
380
|
-
</tbody>
|
|
381
|
-
</table>
|
|
382
|
-
</div>
|
|
362
|
+
<Table
|
|
363
|
+
:columns="MEMBER_COLUMNS"
|
|
364
|
+
:data="memberRows()"
|
|
365
|
+
:loading="isLoading()"
|
|
366
|
+
:row-actions="MEMBER_ACTIONS"
|
|
367
|
+
action-label-key="name"
|
|
368
|
+
busy-key="id"
|
|
369
|
+
:busy-value="busyMemberId()"
|
|
370
|
+
paginated
|
|
371
|
+
:page-size="25"
|
|
372
|
+
empty-title="No members yet"
|
|
373
|
+
empty-description="No members have joined this team yet."
|
|
374
|
+
@row-action="handleMemberAction"
|
|
375
|
+
>
|
|
376
|
+
<template #cell="{ row, column }">
|
|
377
|
+
<template :if="column.key === 'name'">
|
|
378
|
+
<div class="flex gap-3 items-center">
|
|
379
|
+
<div class="flex items-center justify-center shrink-0 h-9 w-9 font-semibold text-blue-700 text-sm dark:text-blue-300 bg-blue-50 dark:bg-blue-500/10 rounded-full">{{ row.initial }}</div>
|
|
380
|
+
<div>
|
|
381
|
+
<p class="font-medium text-neutral-900 text-sm dark:text-white">{{ row.name }}</p>
|
|
382
|
+
<p class="mt-0.5 text-neutral-500 text-xs dark:text-neutral-400">{{ row.email }}</p>
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
</template>
|
|
386
|
+
<template :if="column.key === 'role' && row.role === 'owner'">
|
|
387
|
+
<span class="text-neutral-700 text-sm dark:text-neutral-300">Owner</span>
|
|
388
|
+
</template>
|
|
389
|
+
<template :if="column.key === 'role' && row.role !== 'owner'">
|
|
390
|
+
<select
|
|
391
|
+
:value="row.role"
|
|
392
|
+
:aria-label="'Role for ' + row.name"
|
|
393
|
+
:disabled="busyMemberId() === row.id"
|
|
394
|
+
class="px-2.5 h-8 text-neutral-800 text-sm dark:text-neutral-100 bg-white dark:bg-neutral-950 border border-neutral-300 rounded-lg dark:border-neutral-700"
|
|
395
|
+
@change="updateMemberRole(row, $event)"
|
|
396
|
+
>
|
|
397
|
+
<option value="admin">Admin</option>
|
|
398
|
+
<option value="member">Member</option>
|
|
399
|
+
<option value="viewer">Viewer</option>
|
|
400
|
+
</select>
|
|
401
|
+
</template>
|
|
402
|
+
</template>
|
|
403
|
+
</Table>
|
|
383
404
|
</section>
|
|
384
405
|
|
|
385
406
|
<section class="overflow-hidden bg-white dark:bg-neutral-900 border border-neutral-200/70 rounded-xl dark:border-neutral-800">
|
|
@@ -388,57 +409,20 @@ onMount(() => {
|
|
|
388
409
|
<p class="mt-0.5 text-neutral-500 text-sm dark:text-neutral-400">Track delivery, resend links, or revoke access before acceptance.</p>
|
|
389
410
|
</div>
|
|
390
411
|
|
|
391
|
-
<
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
<tbody class="divide-neutral-200 divide-y dark:divide-neutral-800">
|
|
406
|
-
<template :for="invitation in invitations">
|
|
407
|
-
<tr>
|
|
408
|
-
<td class="px-6 py-4 text-neutral-900 text-sm dark:text-neutral-100">{{ invitation.email }}</td>
|
|
409
|
-
<td class="px-6 py-4 text-neutral-600 text-sm dark:text-neutral-300">{{ roleLabel(invitation.role) }}</td>
|
|
410
|
-
<td class="px-6 py-4">
|
|
411
|
-
<span class="inline-flex px-2 py-1 font-medium text-xs rounded-full" :class="statusClass(invitation.deliveryStatus)">
|
|
412
|
-
{{ roleLabel(invitation.deliveryStatus) }}
|
|
413
|
-
</span>
|
|
414
|
-
</td>
|
|
415
|
-
<td class="px-6 py-4 text-neutral-500 text-sm dark:text-neutral-400">{{ formatDate(invitation.expiresAt) }}</td>
|
|
416
|
-
<td class="px-6 py-4">
|
|
417
|
-
<div class="flex gap-2 items-center justify-end">
|
|
418
|
-
<Button
|
|
419
|
-
variant="secondary"
|
|
420
|
-
size="sm"
|
|
421
|
-
:loading="busyInvitationId() === invitation.id"
|
|
422
|
-
:disabled="busyInvitationId() === invitation.id"
|
|
423
|
-
@click="resendInvitation(invitation)"
|
|
424
|
-
>
|
|
425
|
-
Resend
|
|
426
|
-
</Button>
|
|
427
|
-
<Button
|
|
428
|
-
variant="ghost"
|
|
429
|
-
size="sm"
|
|
430
|
-
:disabled="busyInvitationId() === invitation.id"
|
|
431
|
-
@click="invitationToRevoke.set(invitation)"
|
|
432
|
-
>
|
|
433
|
-
Revoke
|
|
434
|
-
</Button>
|
|
435
|
-
</div>
|
|
436
|
-
</td>
|
|
437
|
-
</tr>
|
|
438
|
-
</template>
|
|
439
|
-
</tbody>
|
|
440
|
-
</table>
|
|
441
|
-
</div>
|
|
412
|
+
<Table
|
|
413
|
+
:columns="INVITATION_COLUMNS"
|
|
414
|
+
:data="invitationRows()"
|
|
415
|
+
:loading="isLoading()"
|
|
416
|
+
:row-actions="INVITATION_ACTIONS"
|
|
417
|
+
action-label-key="email"
|
|
418
|
+
busy-key="id"
|
|
419
|
+
:busy-value="busyInvitationId()"
|
|
420
|
+
paginated
|
|
421
|
+
:page-size="25"
|
|
422
|
+
empty-title="No pending invitations"
|
|
423
|
+
empty-description="Invitations you send appear here until they are accepted."
|
|
424
|
+
@row-action="handleInvitationAction"
|
|
425
|
+
/>
|
|
442
426
|
</section>
|
|
443
427
|
</div>
|
|
444
428
|
|