@sparkelf/dsh-knowledge-base 0.1.0 → 0.1.1
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/lib/client.js +4302 -1970
- package/lib/client.js.map +1 -1
- package/lib/index.js +293 -12
- package/lib/types/api.d.ts +46 -0
- package/lib/types/client/KnowledgeContextMenu.d.ts +31 -6
- package/lib/types/client/KnowledgeFileTree.d.ts +29 -0
- package/lib/types/client/KnowledgeHome.d.ts +23 -0
- package/lib/types/client/KnowledgeListing.d.ts +14 -0
- package/lib/types/client/KnowledgePermissionOverview.d.ts +51 -3
- package/lib/types/client/KnowledgeProgressPanel.d.ts +53 -0
- package/lib/types/client/KnowledgeReferencePicker.d.ts +16 -0
- package/lib/types/client/auditPaging.d.ts +40 -0
- package/lib/types/client/index.d.ts +14 -1
- package/lib/types/client/insertReadiness.d.ts +60 -0
- package/lib/types/client/locales.d.ts +68 -1
- package/lib/types/client/paging.d.ts +69 -0
- package/lib/types/client/permissionRows.d.ts +176 -5
- package/lib/types/client/progressRules.d.ts +54 -0
- package/lib/types/client/referenceTargets.d.ts +60 -0
- package/lib/types/client/shareRelations.d.ts +81 -0
- package/lib/types/client/useFileDrop.d.ts +171 -0
- package/lib/types/client/useFileDropHook.d.ts +64 -0
- package/lib/types/extractionQueue.d.ts +154 -0
- package/lib/types/index.d.ts +53 -0
- package/package.json +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The progress strip at the bottom of the knowledge base panel.
|
|
3
|
+
*
|
|
4
|
+
* Uploading a document and extracting its text are two waits a reader cannot see
|
|
5
|
+
* otherwise: the drop appears to do nothing until the bytes land, and a scanned PDF
|
|
6
|
+
* looks uploaded while its `.md` sibling is still being produced. Both are reported
|
|
7
|
+
* here, in one place, at the bottom of the surface the reader is already looking at.
|
|
8
|
+
*
|
|
9
|
+
* It is a resident strip rather than a toast on purpose. A toast is gone before a slow
|
|
10
|
+
* parse finishes, and the reader's next question ("did it work?") has nowhere to go;
|
|
11
|
+
* this stays until the work is done and then folds to one line so it stops taking
|
|
12
|
+
* space without taking the record away.
|
|
13
|
+
*
|
|
14
|
+
* Nothing here is persisted. A refresh loses the history, which is the right trade: the
|
|
15
|
+
* files themselves are on disk, and a stale "uploading 40%" from yesterday would be a
|
|
16
|
+
* lie the reader cannot clear.
|
|
17
|
+
*
|
|
18
|
+
* @module @sparkelf/dsh-knowledge-base/client
|
|
19
|
+
*/
|
|
20
|
+
/** One file's upload, as the strip shows it. */
|
|
21
|
+
export interface UploadTaskView {
|
|
22
|
+
/** The file's path relative to the drop target: the name the reader dropped. */
|
|
23
|
+
name: string;
|
|
24
|
+
/** The tree-relative directory it is landing in, `''` for the mount root. */
|
|
25
|
+
dir: string;
|
|
26
|
+
/** Where this file is in its own lifecycle. */
|
|
27
|
+
state: 'waiting' | 'uploading' | 'done' | 'failed';
|
|
28
|
+
/** Why it failed, in the Host's own words. */
|
|
29
|
+
reason?: string;
|
|
30
|
+
}
|
|
31
|
+
/** Props for the progress strip. */
|
|
32
|
+
export interface KnowledgeProgressPanelProps {
|
|
33
|
+
/** Locale-bound copy resolver. */
|
|
34
|
+
t: (key: string) => string;
|
|
35
|
+
/** The batch currently uploading, oldest first; empty when nothing is in flight. */
|
|
36
|
+
uploads: readonly UploadTaskView[];
|
|
37
|
+
/**
|
|
38
|
+
* Whether to read extraction progress at all.
|
|
39
|
+
*
|
|
40
|
+
* False on a surface that cannot extract (no mount, or a caller without write
|
|
41
|
+
* access): polling a route whose answer can never be acted on is a request per
|
|
42
|
+
* reader that buys nothing.
|
|
43
|
+
*/
|
|
44
|
+
watchExtraction: boolean;
|
|
45
|
+
/** Called after a retry was accepted, so the caller can refresh its own views. */
|
|
46
|
+
onRetried?: (relPath: string) => void;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Render the bottom progress strip.
|
|
50
|
+
* @param props - the upload batch, whether to watch extraction, and the copy resolver.
|
|
51
|
+
* @returns The strip, or nothing when there is no work to show.
|
|
52
|
+
*/
|
|
53
|
+
export declare function KnowledgeProgressPanel(props: KnowledgeProgressPanelProps): import("react").JSX.Element | null;
|
|
@@ -33,6 +33,22 @@ export interface KnowledgeReferencePickerProps {
|
|
|
33
33
|
path: string;
|
|
34
34
|
/** Whether that entry is a directory (a folder mention rather than a file chip). */
|
|
35
35
|
isDir: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Every entry a multi-row selection asked to reference.
|
|
38
|
+
*
|
|
39
|
+
* Present only when the `@` button was pressed with several rows selected; the
|
|
40
|
+
* single-row case keeps using `path`/`isDir`. Each entry carries its own `isDir`
|
|
41
|
+
* because a selection may mix folders and documents, and the mention spelling
|
|
42
|
+
* differs between them.
|
|
43
|
+
*
|
|
44
|
+
* This is one dialog for both cases on purpose: "where should this go" is the same
|
|
45
|
+
* question whether one row or five are being referenced, and a second batch-only
|
|
46
|
+
* dialog would be a second place for the insertion contract to drift.
|
|
47
|
+
*/
|
|
48
|
+
targets?: readonly {
|
|
49
|
+
path: string;
|
|
50
|
+
isDir: boolean;
|
|
51
|
+
}[];
|
|
36
52
|
/** The client context; both the draft insert and the service lookups need it. */
|
|
37
53
|
ctx: unknown;
|
|
38
54
|
/** Locale-bound copy for this package's namespace. */
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordering for the audit trail.
|
|
3
|
+
*
|
|
4
|
+
* Only the ordering lives here. Paging is the shared `paging.ts` utility that the other
|
|
5
|
+
* tables in this package use, adopted rather than duplicated: two implementations of
|
|
6
|
+
* "slice a page and clamp it" drift, and the one that is not used by the table next door
|
|
7
|
+
* is the one that gets the fix.
|
|
8
|
+
*
|
|
9
|
+
* The order is a correctness rule, not a presentation detail. The Host happens to append
|
|
10
|
+
* newest-last to its log, so a table that drew the array as received would look right
|
|
11
|
+
* until the writer changed. The reader's question is "what happened most recently", and
|
|
12
|
+
* that answer is computed here from the timestamps.
|
|
13
|
+
*
|
|
14
|
+
* @module @sparkelf/dsh-knowledge-base/client
|
|
15
|
+
*/
|
|
16
|
+
/** The fields the ordering rule needs. */
|
|
17
|
+
export interface AuditRow {
|
|
18
|
+
/** ISO-8601 instant the row records. */
|
|
19
|
+
at: string;
|
|
20
|
+
/** Path the action concerned, used as the tie-breaker. */
|
|
21
|
+
relPath: string;
|
|
22
|
+
/** What was done, used as the second tie-breaker. */
|
|
23
|
+
action: string;
|
|
24
|
+
/** Destination for a move, used as the last tie-breaker. */
|
|
25
|
+
toRelPath?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Order rows newest first, with a stable tie-break.
|
|
29
|
+
*
|
|
30
|
+
* The secondary keys are what stop rows from swapping places between renders when several
|
|
31
|
+
* share an instant -- the normal case for a batch, where one action writes several rows
|
|
32
|
+
* with the same `at`. Without them the platform sort could order equal keys either way,
|
|
33
|
+
* and a reader paging forward would see a row twice or not at all.
|
|
34
|
+
*
|
|
35
|
+
* Ordering is total: `relPath`, then `action`, then `toRelPath` are all compared, and two
|
|
36
|
+
* rows equal in every one of those are indistinguishable to this table anyway.
|
|
37
|
+
* @param rows - the rows as received.
|
|
38
|
+
* @returns A new array; the input is not mutated.
|
|
39
|
+
*/
|
|
40
|
+
export declare function sortAuditRows<T extends AuditRow>(rows: readonly T[]): T[];
|
|
@@ -18,7 +18,20 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
18
18
|
'settings.knowledgeBase': KnowledgeKey;
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Client services required for these contributions.
|
|
23
|
+
*
|
|
24
|
+
* `sessions` and `workspaces` are here because the `@`-reference dialog inserts into a
|
|
25
|
+
* conversation draft, and that insert resolves the target session's scope through
|
|
26
|
+
* `ctx.sessions` before it can write anything. Without the injection the service is not
|
|
27
|
+
* on this plugin's ctx at all: the call throws `cannot get property "sessions" without
|
|
28
|
+
* inject`, `referenceInChat` catches it into a `console.warn`, and the reader sees a
|
|
29
|
+
* dialog that closes and inserts nothing -- the exact "clicked, nothing happened"
|
|
30
|
+
* symptom this injection fixes at the root rather than by reporting the failure.
|
|
31
|
+
*
|
|
32
|
+
* `workspaces` is the other half: the mention is spelled relative to the workspace that
|
|
33
|
+
* owns the session, so the insert needs that root to project the path.
|
|
34
|
+
*/
|
|
22
35
|
export declare const inject: string[];
|
|
23
36
|
/**
|
|
24
37
|
* Register the knowledge-base navigation entry and Settings section.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When to keep waiting for a freshly created conversation, and when to give up.
|
|
3
|
+
*
|
|
4
|
+
* Lifted out of the picker's `createNew` because the part worth testing is a decision,
|
|
5
|
+
* not a render: "retry, insert, or report failure" is a pure function of two booleans
|
|
6
|
+
* and a clock, and the failure mode when it is wrong is silence — the dialog closes and
|
|
7
|
+
* nothing happens, which is exactly the bug class this whole surface exists to end.
|
|
8
|
+
*
|
|
9
|
+
* @module @sparkelf/dsh-knowledge-base/client
|
|
10
|
+
*/
|
|
11
|
+
/** What one readiness attempt found. */
|
|
12
|
+
export interface ReadinessProbe {
|
|
13
|
+
/** Whether a session id could be resolved at all (the catalog has it). */
|
|
14
|
+
hasTarget: boolean;
|
|
15
|
+
/** Whether the insert actually landed (the workspace could resolve the root). */
|
|
16
|
+
inserted: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** What the caller should do next. */
|
|
19
|
+
export type ReadinessAction =
|
|
20
|
+
/** Try again shortly: the ordinary race, not a refusal. */
|
|
21
|
+
'retry'
|
|
22
|
+
/** Everything landed. */
|
|
23
|
+
| 'done'
|
|
24
|
+
/** Out of time with no session to target. */
|
|
25
|
+
| 'failed-no-target'
|
|
26
|
+
/** Out of time with a session but nothing inserted. */
|
|
27
|
+
| 'failed-insert';
|
|
28
|
+
/**
|
|
29
|
+
* Decide the next move for one readiness attempt.
|
|
30
|
+
*
|
|
31
|
+
* The deadline is passed in rather than read here so the decision is reproducible in a
|
|
32
|
+
* test: a function that read the clock itself could only be tested by waiting.
|
|
33
|
+
*
|
|
34
|
+
* The ordering matters. "No target yet" and "target but no insert" are different
|
|
35
|
+
* failures and get different messages; collapsing them would tell a reader "could not
|
|
36
|
+
* insert" when in fact no conversation was ever identified.
|
|
37
|
+
* @param probe - what this attempt observed.
|
|
38
|
+
* @param expired - whether the wait budget is already spent.
|
|
39
|
+
* @returns The action the caller should take.
|
|
40
|
+
*/
|
|
41
|
+
export declare function decideReadiness(probe: ReadinessProbe, expired: boolean): ReadinessAction;
|
|
42
|
+
/**
|
|
43
|
+
* Whether the wait budget is spent.
|
|
44
|
+
*
|
|
45
|
+
* Split out and tested because the naive version of this comparison is what broke: a
|
|
46
|
+
* deadline computed inside the retry function is recomputed on every pass, so it never
|
|
47
|
+
* expires and the "report a failure" branch becomes unreachable. A test that pins
|
|
48
|
+
* `deadline === now` to `true` is the cheapest way to keep that from coming back.
|
|
49
|
+
* @param now - current epoch milliseconds.
|
|
50
|
+
* @param deadline - the instant the budget runs out.
|
|
51
|
+
* @returns True once `now` has reached the deadline.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isExpired(now: number, deadline: number): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* The deadline for a wait that starts now.
|
|
56
|
+
* @param now - current epoch milliseconds.
|
|
57
|
+
* @param budgetMs - how long the caller is willing to wait.
|
|
58
|
+
* @returns The instant the budget runs out.
|
|
59
|
+
*/
|
|
60
|
+
export declare function deadlineFrom(now: number, budgetMs: number): number;
|
|
@@ -73,7 +73,7 @@ export declare const en: {
|
|
|
73
73
|
readonly homeTemplateBody: "Start from a template the team already uses.";
|
|
74
74
|
readonly homeTabRecent: "Recently opened";
|
|
75
75
|
readonly homeTabMine: "Owned by me";
|
|
76
|
-
readonly homeTabShared: "
|
|
76
|
+
readonly homeTabShared: "Shares";
|
|
77
77
|
readonly homeTabStarred: "Starred";
|
|
78
78
|
readonly homeColTitle: "Title";
|
|
79
79
|
readonly homeColLocation: "Location";
|
|
@@ -342,6 +342,73 @@ export declare const en: {
|
|
|
342
342
|
readonly 'step.sshfs-mount': "Mount the share over SSHFS";
|
|
343
343
|
readonly 'step.sshfs-verify': "Verify the SSHFS mount";
|
|
344
344
|
readonly deployLocalOnly: "Deployment steps run on a remote host. This workspace stores documents locally, so there is nothing to deploy.";
|
|
345
|
+
readonly shareGroupReceived: "Inbound";
|
|
346
|
+
readonly shareGroupGiven: "Outbound";
|
|
347
|
+
readonly shareColRelation: "Sharing";
|
|
348
|
+
readonly shareColAudience: "Shared with";
|
|
349
|
+
readonly shareColPermission: "Permission";
|
|
350
|
+
readonly shareReceivedEmpty: "Nothing has been shared with you yet.";
|
|
351
|
+
readonly shareGivenEmpty: "You have not shared anything yet.";
|
|
352
|
+
readonly shareGivenRelation: "Me → {audience}";
|
|
353
|
+
readonly shareReceivedRelation: "From resource owner";
|
|
354
|
+
readonly shareUnknownAudience: "Unknown principal";
|
|
355
|
+
readonly refPickerBack: "Back";
|
|
356
|
+
readonly actionFavoriteMany: "Add {n} to favourites";
|
|
357
|
+
readonly actionUnfavoriteMany: "Remove {n} from favourites";
|
|
358
|
+
readonly actionCopyMany: "Copy {n} items";
|
|
359
|
+
readonly actionCutMany: "Cut {n} items";
|
|
360
|
+
readonly actionDeleteMany: "Delete {n} items (move to trash)";
|
|
361
|
+
readonly refPickerBatchTitle: "Reference {n} items in a chat";
|
|
362
|
+
readonly refPickerFailed: "Could not insert the reference into that chat.";
|
|
363
|
+
readonly dropHomeHint: "Drop to upload into your personal folder";
|
|
364
|
+
readonly progressTitle: "Progress";
|
|
365
|
+
readonly progressRoot: "Root";
|
|
366
|
+
readonly overviewByRole: "By role";
|
|
367
|
+
readonly sourceDirect: "Direct";
|
|
368
|
+
readonly sourceViaRole: "Via role {role}";
|
|
369
|
+
readonly sourceUnavailable: "Inheritance unavailable (requires admin access)";
|
|
370
|
+
readonly emptyByPerson: "No account is granted access directly.";
|
|
371
|
+
readonly emptyByRole: "No role is granted access.";
|
|
372
|
+
readonly emptyByResource: "No resource is granted access.";
|
|
373
|
+
readonly actionsUnknown: "Unknown";
|
|
374
|
+
readonly actionsUnknownHint: "Could not determine the full permission set";
|
|
375
|
+
readonly colAccount: "Account";
|
|
376
|
+
readonly colSource: "Source";
|
|
377
|
+
readonly roleMemberCount: "{count} member(s)";
|
|
378
|
+
readonly homeStarredEmpty: "Nothing starred yet.";
|
|
379
|
+
readonly menuExtractMarkdown: "Extract Markdown";
|
|
380
|
+
readonly extractRunning: "Extracting…";
|
|
381
|
+
readonly extractDone: "Markdown extracted";
|
|
382
|
+
readonly extractExists: "Markdown already exists";
|
|
383
|
+
readonly extractUnsupported: "This file type cannot be extracted";
|
|
384
|
+
readonly extractFailed: "Extraction failed: {reason}";
|
|
385
|
+
readonly dropFailed: "{ok} uploaded, {n} failed: {names}";
|
|
386
|
+
readonly dropDismiss: "Dismiss";
|
|
387
|
+
readonly dropUploading: "Uploading…";
|
|
388
|
+
readonly dropNeedsFolder: "Drop onto a folder";
|
|
389
|
+
readonly dropNoFiles: "No files to upload";
|
|
390
|
+
readonly progressUploadGroup: "Uploads";
|
|
391
|
+
readonly progressExtractGroup: "Extractions";
|
|
392
|
+
readonly progressUploading: "Uploading {done}/{total}";
|
|
393
|
+
readonly progressPending: "Queued";
|
|
394
|
+
readonly progressExtracting: "Extracting";
|
|
395
|
+
readonly progressExtractingVia: "Extracting ({via})";
|
|
396
|
+
readonly progressSucceeded: "Done";
|
|
397
|
+
readonly progressFailed: "Failed: {reason}";
|
|
398
|
+
readonly progressUnsupported: "This file type has no extractor";
|
|
399
|
+
readonly progressRetry: "Retry";
|
|
400
|
+
readonly progressCollapse: "Collapse";
|
|
401
|
+
readonly progressExpand: "Expand";
|
|
402
|
+
readonly progressSummary: "{uploaded} uploaded, {failed} failed";
|
|
403
|
+
readonly progressSummaryExtract: "{done} extracted, {failed} failed";
|
|
404
|
+
readonly progressRetrying: "Retrying…";
|
|
405
|
+
readonly progressRetryFailed: "Retry failed: {reason}";
|
|
406
|
+
readonly auditPagePrev: "Previous";
|
|
407
|
+
readonly auditPageNext: "Next";
|
|
408
|
+
readonly auditPageOf: "Page {page} of {pages}";
|
|
409
|
+
readonly auditPageSize: "Rows";
|
|
410
|
+
readonly auditPageSizeOption: "{size}";
|
|
411
|
+
readonly auditPageTotal: "{total} entries";
|
|
345
412
|
readonly settingsMineruHeading: "MinerU service address";
|
|
346
413
|
readonly settingsMineruHint: "Where documents are parsed. Leave it empty to use the port this deployment was configured with.";
|
|
347
414
|
readonly settingsMineruPlaceholder: "Leave empty to derive from the configured port";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page slicing for every table in the knowledge surface.
|
|
3
|
+
*
|
|
4
|
+
* A pure module rather than a hook: the rule worth testing is arithmetic, and a hook
|
|
5
|
+
* would need a renderer to exercise. The component owns the page and size as state;
|
|
6
|
+
* this answers what that state should display.
|
|
7
|
+
*
|
|
8
|
+
* The one rule that matters is the clamp. A reader can be on page 7 of a table when a
|
|
9
|
+
* filter, a search word or a re-read shrinks it to two pages, and obeying "page 7"
|
|
10
|
+
* literally renders an empty table with a pager that still says there is data. So the
|
|
11
|
+
* requested page is clamped into range and the clamped value is returned, which is what
|
|
12
|
+
* the owner writes back into its own state.
|
|
13
|
+
*
|
|
14
|
+
* @module @sparkelf/dsh-knowledge-base/client/paging
|
|
15
|
+
*/
|
|
16
|
+
/** Page sizes every table offers, in the order the control shows them. */
|
|
17
|
+
export declare const PAGE_SIZES: readonly [20, 50, 100];
|
|
18
|
+
/** The page size a fresh table starts on. */
|
|
19
|
+
export declare const DEFAULT_PAGE_SIZE = 20;
|
|
20
|
+
/** One page of rows plus the numbers a pager draws. */
|
|
21
|
+
export interface Page<T> {
|
|
22
|
+
/** The rows on this page, in order. */
|
|
23
|
+
rows: T[];
|
|
24
|
+
/** 1-based page actually shown, clamped into range. */
|
|
25
|
+
page: number;
|
|
26
|
+
/** Total number of pages; never zero, so no pager can say "page 1 of 0". */
|
|
27
|
+
pageCount: number;
|
|
28
|
+
/** Total row count across all pages. */
|
|
29
|
+
total: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Whether a table of this size needs a pager at all.
|
|
33
|
+
*
|
|
34
|
+
* A single page has nothing to page through, so the control row is not drawn: an
|
|
35
|
+
* always-visible pager on a three-row table is furniture that implies the reader is
|
|
36
|
+
* missing something. Empty is the same answer for the same reason.
|
|
37
|
+
* @param total - row count before slicing.
|
|
38
|
+
* @param pageSize - rows per page.
|
|
39
|
+
* @returns true when there is more than one page.
|
|
40
|
+
*/
|
|
41
|
+
export declare function needsPager(total: number, pageSize: number): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Slice one page out of a list that is already in display order.
|
|
44
|
+
*
|
|
45
|
+
* Ordering is the caller's job and happens BEFORE this call: pagination must never
|
|
46
|
+
* reorder, or turning a page would appear to sort the table differently. The same
|
|
47
|
+
* applies to filtering -- a filtered list is what gets paged, so the count in the pager
|
|
48
|
+
* is the count of matches rather than of the whole table.
|
|
49
|
+
*
|
|
50
|
+
* @param rows - rows already filtered and ordered.
|
|
51
|
+
* @param page - 1-based page requested; clamped into range.
|
|
52
|
+
* @param pageSize - rows per page; a non-positive or non-integer value falls back.
|
|
53
|
+
* @returns The page and the numbers to draw.
|
|
54
|
+
*/
|
|
55
|
+
export declare function paginate<T>(rows: readonly T[], page: number, pageSize: number): Page<T>;
|
|
56
|
+
/**
|
|
57
|
+
* The 1-based page a size change should land on.
|
|
58
|
+
*
|
|
59
|
+
* Keeping the first visible row in view is the point: a reader who switches from 20 to
|
|
60
|
+
* 100 rows is looking at row 41, so the page they want is the one containing it, not
|
|
61
|
+
* page 1 and not page 3 of the old numbering. Clamping in {@link paginate} then keeps
|
|
62
|
+
* that answer in range if the list is shorter than expected.
|
|
63
|
+
*
|
|
64
|
+
* @param currentPage - page shown before the change.
|
|
65
|
+
* @param previousSize - size in force before the change.
|
|
66
|
+
* @param nextSize - size being chosen.
|
|
67
|
+
* @returns The 1-based page to request.
|
|
68
|
+
*/
|
|
69
|
+
export declare function pageAfterSizeChange(currentPage: number, previousSize: number, nextSize: number): number;
|
|
@@ -50,16 +50,64 @@ export interface ResourceHoldings {
|
|
|
50
50
|
grants: GrantView[];
|
|
51
51
|
}>;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* The result of reading a set of grants as permissions.
|
|
55
|
+
*
|
|
56
|
+
* `known: false` carries a reason rather than an empty list, because "we could not
|
|
57
|
+
* work this out" and "this principal holds nothing" must not render the same way: the
|
|
58
|
+
* first is a fault the reader should see, and collapsing it into an empty set is how a
|
|
59
|
+
* denial gets invented. See {@link resolveActions}.
|
|
60
|
+
*/
|
|
61
|
+
export interface ResolvedActions {
|
|
62
|
+
/** Actions held, in display order. Empty when `known` is false. */
|
|
63
|
+
actions: string[];
|
|
64
|
+
/**
|
|
65
|
+
* Whether the full set could be determined.
|
|
66
|
+
*
|
|
67
|
+
* False when part of the input could not be read -- an inheritance path that was
|
|
68
|
+
* unavailable, or a grant whose action is a word this build does not know.
|
|
69
|
+
*/
|
|
70
|
+
known: boolean;
|
|
71
|
+
/** Why the set is incomplete, when it is. Shown to the reader beside the actions. */
|
|
72
|
+
reason?: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Union a principal's grants into the permission set it actually holds.
|
|
76
|
+
*
|
|
77
|
+
* UNION, never intersection and never "the first row": a principal reaches a resource
|
|
78
|
+
* through several grants (a direct one and one inherited from each role), and what
|
|
79
|
+
* they may do is everything any of those grants allows. Reading one row is what made
|
|
80
|
+
* an administrator display as read-only.
|
|
81
|
+
*
|
|
82
|
+
* An unrecognised action word makes the whole answer unknown rather than being
|
|
83
|
+
* skipped. Skipping it would under-report, and under-reporting a permission is the
|
|
84
|
+
* direction that misleads an operator into thinking access is safe to ignore.
|
|
85
|
+
*
|
|
86
|
+
* @param actions - every action string from every grant reaching this principal.
|
|
87
|
+
* @returns The unioned set, or an explicit unknown with the reason.
|
|
88
|
+
*/
|
|
89
|
+
export declare function resolveActions(actions: readonly string[]): ResolvedActions;
|
|
90
|
+
/**
|
|
91
|
+
* Whether a resolved set confers the ability to change a document.
|
|
92
|
+
*
|
|
93
|
+
* Used by the page to check itself against what the reader can actually do. `manage`
|
|
94
|
+
* implies write in this model: it is the full-control level, and a principal holding
|
|
95
|
+
* it can move and delete.
|
|
96
|
+
* @param resolved - the unioned permission set.
|
|
97
|
+
* @returns true when writing is possible, or unknown.
|
|
98
|
+
*/
|
|
99
|
+
export declare function canWriteWith(resolved: ResolvedActions): boolean | undefined;
|
|
53
100
|
/**
|
|
54
101
|
* Collapse a set of action strings into the shortest describing list.
|
|
55
102
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* expectations as the server's own test.
|
|
103
|
+
* A thin view over {@link resolveActions} so there is ONE vocabulary and ONE union
|
|
104
|
+
* rule in the browser: this used to be a second implementation filtering on
|
|
105
|
+
* `create`/`update`/`delete`, which silently dropped every `write` and `manage` grant.
|
|
60
106
|
*
|
|
107
|
+
* Kept as an exported name because the older callers and their tests use it.
|
|
61
108
|
* @param actions - raw action strings from grants.
|
|
62
|
-
* @returns
|
|
109
|
+
* @returns The unioned actions in display order; unknown words are dropped here,
|
|
110
|
+
* which is why a surface that must not under-report calls {@link resolveActions}.
|
|
63
111
|
*/
|
|
64
112
|
export declare function summarizeActions(actions: readonly string[]): string[];
|
|
65
113
|
/**
|
|
@@ -92,3 +140,126 @@ export declare function groupHoldingsByResource(grants: readonly GrantView[], la
|
|
|
92
140
|
* @returns The formatted instant, or a dash when there is nothing to show.
|
|
93
141
|
*/
|
|
94
142
|
export declare function formatGrantInstant(value: string): string;
|
|
143
|
+
/**
|
|
144
|
+
* One DataOps account as the user directory reports it, reduced to what grouping needs.
|
|
145
|
+
*
|
|
146
|
+
* `roles` is what makes inheritance visible: a role grant names the ROLE, so without
|
|
147
|
+
* the membership list the page can only show role grants as belonging to nobody. The
|
|
148
|
+
* directory is the only source for it, and it is administrator-only -- see
|
|
149
|
+
* {@link DirectoryState}.
|
|
150
|
+
*/
|
|
151
|
+
export interface DirectoryUser {
|
|
152
|
+
/** DataOps user id, which is what a `user` grant names. */
|
|
153
|
+
userId: string;
|
|
154
|
+
/** Login name, shown beside the display name so two "张伟" can be told apart. */
|
|
155
|
+
username: string;
|
|
156
|
+
/** Display name, when the account has one. */
|
|
157
|
+
displayName: string;
|
|
158
|
+
/** Role codes this account belongs to. */
|
|
159
|
+
roleCodes: string[];
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Whether the user directory could be read.
|
|
163
|
+
*
|
|
164
|
+
* Three states, not two. "Nobody holds a role" and "we could not look" produce the same
|
|
165
|
+
* empty membership map, and rendering them the same way is how a page ends up telling
|
|
166
|
+
* an operator that nobody has inherited access when the truth is that the read failed.
|
|
167
|
+
*/
|
|
168
|
+
export type DirectoryState = {
|
|
169
|
+
kind: 'available';
|
|
170
|
+
users: readonly DirectoryUser[];
|
|
171
|
+
} | {
|
|
172
|
+
kind: 'unavailable';
|
|
173
|
+
reason: string;
|
|
174
|
+
};
|
|
175
|
+
/** One account's holdings, as the by-person view renders them. */
|
|
176
|
+
export interface AccountHoldings {
|
|
177
|
+
/** The account id. */
|
|
178
|
+
userId: string;
|
|
179
|
+
/** Login name, for disambiguating identical display names. */
|
|
180
|
+
username?: string;
|
|
181
|
+
/** Display name, or the id when the account is not in the directory. */
|
|
182
|
+
label: string;
|
|
183
|
+
/** Distinct paths this account reaches, directly or through a role. */
|
|
184
|
+
paths: string[];
|
|
185
|
+
/** The unioned permission set, never narrowed by a single grant. */
|
|
186
|
+
resolved: ResolvedActions;
|
|
187
|
+
/** Direct grants the account holds itself. */
|
|
188
|
+
directGrants: GrantView[];
|
|
189
|
+
/**
|
|
190
|
+
* Role codes the account inherits through.
|
|
191
|
+
*
|
|
192
|
+
* Empty when the directory could not be read; {@link inheritanceKnown} says which.
|
|
193
|
+
*/
|
|
194
|
+
viaRoles: string[];
|
|
195
|
+
/** Whether the inheritance half of this row was actually determined. */
|
|
196
|
+
inheritanceKnown: boolean;
|
|
197
|
+
/** Why inheritance is unknown, when it is. */
|
|
198
|
+
inheritanceReason?: string;
|
|
199
|
+
/** Most recent grant instant across the rows this account covers. */
|
|
200
|
+
latestAt: string;
|
|
201
|
+
}
|
|
202
|
+
/** One role's holdings, as the by-role view renders them. */
|
|
203
|
+
export interface RoleHoldings {
|
|
204
|
+
/** Role code, which is what a `role` grant names. */
|
|
205
|
+
roleCode: string;
|
|
206
|
+
/** Role display name when the directory resolved one, else the code. */
|
|
207
|
+
label: string;
|
|
208
|
+
/** Distinct paths the role covers. */
|
|
209
|
+
paths: string[];
|
|
210
|
+
/** The unioned permission set for this role. */
|
|
211
|
+
resolved: ResolvedActions;
|
|
212
|
+
/** Grants that name this role. */
|
|
213
|
+
grants: GrantView[];
|
|
214
|
+
/** How many directory accounts belong to this role, when the directory is known. */
|
|
215
|
+
memberCount?: number;
|
|
216
|
+
/** Most recent grant instant across this role's rows. */
|
|
217
|
+
latestAt: string;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Group grants by the DataOps ACCOUNT that holds them.
|
|
221
|
+
*
|
|
222
|
+
* The account is the row grain because "who can see this" is a question about people.
|
|
223
|
+
* A role grant confers access on every member of that role, so its paths are attributed
|
|
224
|
+
* to each member here -- otherwise a reader whose access is entirely role-derived would
|
|
225
|
+
* appear on no row at all, and the page would say "nobody has access" to somebody who
|
|
226
|
+
* can write.
|
|
227
|
+
*
|
|
228
|
+
* Three sources are merged, and the merge is a UNION:
|
|
229
|
+
* 1. direct `user` grants;
|
|
230
|
+
* 2. grants held by any role the account belongs to (inherited);
|
|
231
|
+
* 3. nothing else -- the built-in administrator levels are already rows in the grant
|
|
232
|
+
* table (`source: 'seed'`), so hard-coding them here would create a second authority
|
|
233
|
+
* that could disagree with the backend.
|
|
234
|
+
*
|
|
235
|
+
* @param grants - the whole grant table.
|
|
236
|
+
* @param directory - the user directory, or the reason it could not be read.
|
|
237
|
+
* @param labels - display names by `principalType:principalId`, optional.
|
|
238
|
+
* @returns One row per account that holds or inherits at least one grant, sorted by label.
|
|
239
|
+
*/
|
|
240
|
+
export declare function groupHoldingsByAccount(grants: readonly GrantView[], directory: DirectoryState, labels?: Readonly<Record<string, string>>): AccountHoldings[];
|
|
241
|
+
/**
|
|
242
|
+
* Group grants by the ROLE they name.
|
|
243
|
+
*
|
|
244
|
+
* The same grants the old "by person" table showed, now labelled as what they are: a
|
|
245
|
+
* role is not a person, and putting it under "by person" told the reader that the
|
|
246
|
+
* administrators were the only people with access.
|
|
247
|
+
*
|
|
248
|
+
* A role whose grants are all unreadable still gets a row with `known: false`: that is
|
|
249
|
+
* the safety net against rendering "no permission" for a read failure.
|
|
250
|
+
*
|
|
251
|
+
* @param grants - the whole grant table.
|
|
252
|
+
* @param labels - display names by `principalType:principalId`, optional.
|
|
253
|
+
* @param memberCounts - role code -> member count, when the directory is known.
|
|
254
|
+
* @returns One row per role named by at least one grant, sorted by label.
|
|
255
|
+
*/
|
|
256
|
+
export declare function groupHoldingsByRole(grants: readonly GrantView[], labels?: Readonly<Record<string, string>>, memberCounts?: ReadonlyMap<string, number>): RoleHoldings[];
|
|
257
|
+
/**
|
|
258
|
+
* Build the role -> member-count map from a directory.
|
|
259
|
+
*
|
|
260
|
+
* Only meaningful when the directory is known; returns undefined otherwise so the row
|
|
261
|
+
* omits the count rather than claiming zero members.
|
|
262
|
+
* @param directory - the user directory state.
|
|
263
|
+
* @returns Counts by role code, or undefined when unavailable.
|
|
264
|
+
*/
|
|
265
|
+
export declare function roleMemberCounts(directory: DirectoryState): Map<string, number> | undefined;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The progress strip's two decisions, kept out of the component.
|
|
3
|
+
*
|
|
4
|
+
* Both are answers about data rather than about React: whether a batch is finished, and
|
|
5
|
+
* whether a retry button would help. They live here because a `.tsx` module that imports
|
|
6
|
+
* the UI primitives cannot be loaded by the test runner (the primitives package imports
|
|
7
|
+
* CSS at module scope, which the runner cannot resolve), so a test that imported the
|
|
8
|
+
* component would fail before reaching any assertion.
|
|
9
|
+
*
|
|
10
|
+
* @module @sparkelf/dsh-knowledge-base/client
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Whether the strip may fold now.
|
|
14
|
+
*
|
|
15
|
+
* Folded means "one summary line instead of a list". It folds when every task in both
|
|
16
|
+
* groups reached a terminal state AND the batch has not been folded before -- the
|
|
17
|
+
* second half is what stops it re-folding after a reader expanded a finished batch to
|
|
18
|
+
* read which file failed.
|
|
19
|
+
* @param uploads - the current batch's rows.
|
|
20
|
+
* @param extractions - the extraction tasks the Host reports.
|
|
21
|
+
* @param alreadyFolded - whether a fold already happened for this batch.
|
|
22
|
+
* @returns Whether to fold.
|
|
23
|
+
*/
|
|
24
|
+
export declare function shouldFoldStrip(uploads: readonly {
|
|
25
|
+
state: string;
|
|
26
|
+
}[], extractions: readonly {
|
|
27
|
+
state: string;
|
|
28
|
+
}[], alreadyFolded: boolean): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Whether a retry button would help for this state.
|
|
31
|
+
*
|
|
32
|
+
* Only a failure is worth retrying. `unsupported` means no extractor exists for the
|
|
33
|
+
* type, so a retry fails identically and the button would be a lie; the pending and
|
|
34
|
+
* running states are already being handled.
|
|
35
|
+
* @param state - the extraction state.
|
|
36
|
+
* @returns Whether to offer a retry.
|
|
37
|
+
*/
|
|
38
|
+
export declare function canRetryExtraction(state: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* A failure reason that survives a response which is not JSON.
|
|
41
|
+
*
|
|
42
|
+
* A gateway answering 502 sends HTML, and `response.json()` rejects on it. If that
|
|
43
|
+
* rejection escaped, the row would report "failed" with no reason at all -- which is
|
|
44
|
+
* exactly the silence this strip exists to remove. So the body is attempted, and an
|
|
45
|
+
* unreadable body falls back to the status code, which still tells the reader whether
|
|
46
|
+
* the server refused them (4xx) or broke (5xx).
|
|
47
|
+
* @param body - the parsed body, when it parsed.
|
|
48
|
+
* @param status - the HTTP status.
|
|
49
|
+
* @returns A sentence a reader can act on.
|
|
50
|
+
*/
|
|
51
|
+
export declare function failureReason(body: {
|
|
52
|
+
error?: unknown;
|
|
53
|
+
code?: unknown;
|
|
54
|
+
} | undefined, status: number): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the reference picker and the batch menu.
|
|
3
|
+
*
|
|
4
|
+
* Kept out of the components because the parts worth testing are the decisions, not the
|
|
5
|
+
* rendering: how a selection becomes a target list, what the batch label says, and which
|
|
6
|
+
* entries a paste loop should attempt. The components are wired to these, so a change in
|
|
7
|
+
* behaviour here is what the tests below pin down; the real DOM interaction is covered by
|
|
8
|
+
* the browser walkthrough instead.
|
|
9
|
+
*
|
|
10
|
+
* @module @sparkelf/dsh-knowledge-base/client
|
|
11
|
+
*/
|
|
12
|
+
/** One entry a reference will be inserted for, in absolute path form. */
|
|
13
|
+
export interface ReferenceTarget {
|
|
14
|
+
/** Absolute path as the tree and the routes speak it. */
|
|
15
|
+
path: string;
|
|
16
|
+
/** True for a directory (folder mention) rather than a document (file chip). */
|
|
17
|
+
isDir: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** One row a batch action covers. */
|
|
20
|
+
export interface BatchRow {
|
|
21
|
+
/** Path relative to the knowledge root. */
|
|
22
|
+
relPath: string;
|
|
23
|
+
/** Display name, for menu copy. */
|
|
24
|
+
name: string;
|
|
25
|
+
/** Whether the row is a directory. */
|
|
26
|
+
isDir: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The entries a picker opened on `relPath` should insert.
|
|
30
|
+
*
|
|
31
|
+
* A multi-row selection wins when it contains the clicked row, because that is the set
|
|
32
|
+
* the reader could see highlighted. Otherwise the click is a single-row action on the row
|
|
33
|
+
* under the pointer -- which is also what happens when the clicked row sits outside the
|
|
34
|
+
* selection, since acting on a batch the reader did not point at would be a surprise.
|
|
35
|
+
* @param relPath - the row the `@` button was pressed on.
|
|
36
|
+
* @param selection - the rows the explorer reports as selected, if any.
|
|
37
|
+
* @returns One target, or every selected row when the click was inside the selection.
|
|
38
|
+
*/
|
|
39
|
+
export declare function referenceTargetsFor(relPath: string, selection?: readonly BatchRow[]): ReferenceTarget[];
|
|
40
|
+
/**
|
|
41
|
+
* The rows a menu opened on one row should act on.
|
|
42
|
+
*
|
|
43
|
+
* Mirrors {@link referenceTargetsFor}: a right-click inside a multi-selection acts on the
|
|
44
|
+
* whole selection, and anywhere else acts on the one row. The menu and the picker must
|
|
45
|
+
* agree, or a "delete 3 items" dialog would reference four.
|
|
46
|
+
* @param target - the row under the pointer.
|
|
47
|
+
* @param selection - the rows marked selected, if any.
|
|
48
|
+
* @returns The rows the batch entries cover, or an empty list for a single-row menu.
|
|
49
|
+
*/
|
|
50
|
+
export declare function batchRowsFor(target: BatchRow, selection?: readonly BatchRow[]): BatchRow[];
|
|
51
|
+
/**
|
|
52
|
+
* Fill a batch label's `{n}` placeholder.
|
|
53
|
+
*
|
|
54
|
+
* A separate function so the count is formatted in one place: a label that prints the
|
|
55
|
+
* rows it will touch must never print a different number from the rows it does touch.
|
|
56
|
+
* @param template - the locale string, containing `{n}`.
|
|
57
|
+
* @param count - how many rows.
|
|
58
|
+
* @returns The label with the count substituted.
|
|
59
|
+
*/
|
|
60
|
+
export declare function batchLabel(template: string, count: number): string;
|