@crvouga/atlas 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/reports/src/evidence-detail.d.ts +23 -0
- package/dist/reports/src/evidence-detail.js +19 -1
- package/dist/reports/src/export-evidence.js +1 -1
- package/dist/reports/src/layers-route.d.ts +2 -1
- package/dist/reports/src/layers-route.js +8 -4
- package/dist/reports/src/test-catalog.d.ts +27 -0
- package/dist/reports/src/test-catalog.js +8 -2
- package/dist/schema/src/api.d.ts +22 -0
- package/dist/schema/src/api.js +10 -1
- package/dist/server/src/openapi.js +2 -2
- package/docs/reference/http-api.md +4 -4
- package/docs/reference/reports.md +64 -7
- package/docs/rich-evidence.md +23 -0
- package/docs/ui.md +1 -1
- package/llms-full.txt +28 -5
- package/package.json +1 -1
- package/packages/schema/schemas/check-detail.schema.json +24 -0
- package/packages/schema/schemas/report-run.schema.json +85 -0
- package/packages/schema/schemas/report-runs.schema.json +6 -0
- package/packages/server/openapi.json +119 -4
- package/packages/visualizer/src/evidence/artifacts.tsx +54 -12
- package/packages/visualizer/src/explorer/Explorer.tsx +9 -5
- package/packages/visualizer/src/explorer/RunArtifacts.tsx +246 -0
- package/packages/visualizer/src/explorer/RunsView.tsx +40 -9
- package/packages/visualizer/src/explorer/search.ts +5 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crvouga/atlas",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Atlas: specs of any kind, reported as layered evidence. Publish checks from any runner (JUnit, CTRF, a script), combine them across nodes, branches, environments and backends, gate CI on the result, and run statecharts against the real app.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -1645,6 +1645,30 @@
|
|
|
1645
1645
|
"additionalProperties": {},
|
|
1646
1646
|
"description": "What the run as a whole carries: its logs, links, labels, annotations, scope and extensions"
|
|
1647
1647
|
},
|
|
1648
|
+
"parts": {
|
|
1649
|
+
"description": "Without `check` only: the parts of the layer that carry files or links of their own, each with what it carries. Absent when none",
|
|
1650
|
+
"type": "array",
|
|
1651
|
+
"items": {
|
|
1652
|
+
"type": "object",
|
|
1653
|
+
"properties": {
|
|
1654
|
+
"kind": {
|
|
1655
|
+
"type": "string"
|
|
1656
|
+
},
|
|
1657
|
+
"id": {
|
|
1658
|
+
"type": "string"
|
|
1659
|
+
},
|
|
1660
|
+
"title": {
|
|
1661
|
+
"type": "string"
|
|
1662
|
+
}
|
|
1663
|
+
},
|
|
1664
|
+
"required": [
|
|
1665
|
+
"kind",
|
|
1666
|
+
"id",
|
|
1667
|
+
"title"
|
|
1668
|
+
],
|
|
1669
|
+
"additionalProperties": {}
|
|
1670
|
+
}
|
|
1671
|
+
},
|
|
1648
1672
|
"inheritedScope": {
|
|
1649
1673
|
"description": "The scope the check inherits: its part's declaration, else its layer's. Absent when neither declared one: unknown, not live or physical",
|
|
1650
1674
|
"type": "object",
|
|
@@ -171,6 +171,12 @@
|
|
|
171
171
|
],
|
|
172
172
|
"additionalProperties": {}
|
|
173
173
|
},
|
|
174
|
+
"attachments": {
|
|
175
|
+
"description": "Files attached to the run itself, in its layers, not to a check. Absent when there are none",
|
|
176
|
+
"type": "integer",
|
|
177
|
+
"exclusiveMinimum": 0,
|
|
178
|
+
"maximum": 9007199254740991
|
|
179
|
+
},
|
|
174
180
|
"artifactTags": {
|
|
175
181
|
"description": "The normalized tags of the attachments of its results, sorted and distinct. Absent when none is tagged, and in reports of older servers",
|
|
176
182
|
"type": "array",
|
|
@@ -431,6 +437,85 @@
|
|
|
431
437
|
],
|
|
432
438
|
"additionalProperties": {}
|
|
433
439
|
}
|
|
440
|
+
},
|
|
441
|
+
"layers": {
|
|
442
|
+
"description": "Every layer of the run, oldest first. Absent from an older server",
|
|
443
|
+
"type": "array",
|
|
444
|
+
"items": {
|
|
445
|
+
"type": "object",
|
|
446
|
+
"properties": {
|
|
447
|
+
"id": {
|
|
448
|
+
"type": "string",
|
|
449
|
+
"description": "The layer id; read its files with `/api/reports/evidence/detail/{backend}/{id}`"
|
|
450
|
+
},
|
|
451
|
+
"backend": {
|
|
452
|
+
"type": "string"
|
|
453
|
+
},
|
|
454
|
+
"producer": {
|
|
455
|
+
"type": "string"
|
|
456
|
+
},
|
|
457
|
+
"environment": {
|
|
458
|
+
"type": [
|
|
459
|
+
"string",
|
|
460
|
+
"null"
|
|
461
|
+
]
|
|
462
|
+
},
|
|
463
|
+
"recordedAtMs": {
|
|
464
|
+
"type": "number"
|
|
465
|
+
},
|
|
466
|
+
"checks": {
|
|
467
|
+
"type": "integer",
|
|
468
|
+
"minimum": 0,
|
|
469
|
+
"maximum": 9007199254740991,
|
|
470
|
+
"description": "How many checks the layer recorded. Zero means evidence only, not a pass"
|
|
471
|
+
},
|
|
472
|
+
"attachments": {
|
|
473
|
+
"type": "integer",
|
|
474
|
+
"minimum": 0,
|
|
475
|
+
"maximum": 9007199254740991,
|
|
476
|
+
"description": "Files the layer carries as a whole, not those of its checks"
|
|
477
|
+
},
|
|
478
|
+
"videos": {
|
|
479
|
+
"type": "object",
|
|
480
|
+
"properties": {
|
|
481
|
+
"references": {
|
|
482
|
+
"type": "integer",
|
|
483
|
+
"minimum": 0,
|
|
484
|
+
"maximum": 9007199254740991,
|
|
485
|
+
"description": "Every reference to a video; identical bytes referenced twice count twice"
|
|
486
|
+
},
|
|
487
|
+
"recordings": {
|
|
488
|
+
"type": "integer",
|
|
489
|
+
"minimum": 0,
|
|
490
|
+
"maximum": 9007199254740991,
|
|
491
|
+
"description": "Distinct playable recordings"
|
|
492
|
+
},
|
|
493
|
+
"unavailable": {
|
|
494
|
+
"type": "integer",
|
|
495
|
+
"minimum": 0,
|
|
496
|
+
"maximum": 9007199254740991,
|
|
497
|
+
"description": "References that name no bytes"
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
"required": [
|
|
501
|
+
"references",
|
|
502
|
+
"recordings",
|
|
503
|
+
"unavailable"
|
|
504
|
+
],
|
|
505
|
+
"additionalProperties": {}
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
"required": [
|
|
509
|
+
"id",
|
|
510
|
+
"backend",
|
|
511
|
+
"producer",
|
|
512
|
+
"environment",
|
|
513
|
+
"recordedAtMs",
|
|
514
|
+
"checks",
|
|
515
|
+
"attachments"
|
|
516
|
+
],
|
|
517
|
+
"additionalProperties": {}
|
|
518
|
+
}
|
|
434
519
|
}
|
|
435
520
|
},
|
|
436
521
|
"required": [
|
|
@@ -179,6 +179,12 @@
|
|
|
179
179
|
],
|
|
180
180
|
"additionalProperties": {}
|
|
181
181
|
},
|
|
182
|
+
"attachments": {
|
|
183
|
+
"description": "Files attached to the run itself, in its layers, not to a check. Absent when there are none",
|
|
184
|
+
"type": "integer",
|
|
185
|
+
"exclusiveMinimum": 0,
|
|
186
|
+
"maximum": 9007199254740991
|
|
187
|
+
},
|
|
182
188
|
"artifactTags": {
|
|
183
189
|
"description": "The normalized tags of the attachments of its results, sorted and distinct. Absent when none is tagged, and in reports of older servers",
|
|
184
190
|
"type": "array",
|
|
@@ -5299,7 +5299,7 @@
|
|
|
5299
5299
|
"Report"
|
|
5300
5300
|
],
|
|
5301
5301
|
"summary": "Every test one run recorded",
|
|
5302
|
-
"description": "One run and its last result for every test, environment and producer it recorded, failures first. Send `format=markdown` for Markdown.\n\nRole needed: `read`.",
|
|
5302
|
+
"description": "One run and its last result for every test, environment and producer it recorded, failures first, and `layers`: each layer of the run with its backend, how many checks it recorded (zero means evidence only, never a pass) and how many files it carries as a whole. Open a layer's files with `/api/reports/evidence/detail/{backend}/{id}`. Send `format=markdown` for Markdown.\n\nRole needed: `read`.",
|
|
5303
5303
|
"x-atlas-role": "read",
|
|
5304
5304
|
"security": [
|
|
5305
5305
|
{
|
|
@@ -5409,7 +5409,7 @@
|
|
|
5409
5409
|
"Report"
|
|
5410
5410
|
],
|
|
5411
5411
|
"summary": "Every test one run recorded",
|
|
5412
|
-
"description": "One run and its last result for every test, environment and producer it recorded, failures first. Send `format=markdown` for Markdown.\n\nScoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.\n\nRole needed: `read`.",
|
|
5412
|
+
"description": "One run and its last result for every test, environment and producer it recorded, failures first, and `layers`: each layer of the run with its backend, how many checks it recorded (zero means evidence only, never a pass) and how many files it carries as a whole. Open a layer's files with `/api/reports/evidence/detail/{backend}/{id}`. Send `format=markdown` for Markdown.\n\nScoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.\n\nRole needed: `read`.",
|
|
5413
5413
|
"x-atlas-role": "read",
|
|
5414
5414
|
"security": [
|
|
5415
5415
|
{
|
|
@@ -6093,7 +6093,7 @@
|
|
|
6093
6093
|
"Report"
|
|
6094
6094
|
],
|
|
6095
6095
|
"summary": "Everything one result carries",
|
|
6096
|
-
"description": "The timeline, annotations, retries, attachments and extensions of one check, and what its run carries as a whole. Without `check`, only the run's. Attachment bytes are fetched from `/api/reports/layers/{id}/blobs/{bundleKey}`.\n\nRole needed: `read`.",
|
|
6096
|
+
"description": "The timeline, annotations, retries, attachments and extensions of one check, and what its run carries as a whole. Without `check`, only the run's, and `parts`: what each part of the layer that carries files or links of its own holds. Attachment bytes are fetched from `/api/reports/layers/{id}/blobs/{bundleKey}`.\n\nRole needed: `read`.",
|
|
6097
6097
|
"x-atlas-role": "read",
|
|
6098
6098
|
"security": [
|
|
6099
6099
|
{
|
|
@@ -6195,7 +6195,7 @@
|
|
|
6195
6195
|
"Report"
|
|
6196
6196
|
],
|
|
6197
6197
|
"summary": "Everything one result carries",
|
|
6198
|
-
"description": "The timeline, annotations, retries, attachments and extensions of one check, and what its run carries as a whole. Without `check`, only the run's. Attachment bytes are fetched from `/api/reports/layers/{id}/blobs/{bundleKey}`.\n\nScoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.\n\nRole needed: `read`.",
|
|
6198
|
+
"description": "The timeline, annotations, retries, attachments and extensions of one check, and what its run carries as a whole. Without `check`, only the run's, and `parts`: what each part of the layer that carries files or links of its own holds. Attachment bytes are fetched from `/api/reports/layers/{id}/blobs/{bundleKey}`.\n\nScoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.\n\nRole needed: `read`.",
|
|
6199
6199
|
"x-atlas-role": "read",
|
|
6200
6200
|
"security": [
|
|
6201
6201
|
{
|
|
@@ -15767,6 +15767,30 @@
|
|
|
15767
15767
|
"additionalProperties": {},
|
|
15768
15768
|
"description": "What the run as a whole carries: its logs, links, labels, annotations, scope and extensions"
|
|
15769
15769
|
},
|
|
15770
|
+
"parts": {
|
|
15771
|
+
"description": "Without `check` only: the parts of the layer that carry files or links of their own, each with what it carries. Absent when none",
|
|
15772
|
+
"type": "array",
|
|
15773
|
+
"items": {
|
|
15774
|
+
"type": "object",
|
|
15775
|
+
"properties": {
|
|
15776
|
+
"kind": {
|
|
15777
|
+
"type": "string"
|
|
15778
|
+
},
|
|
15779
|
+
"id": {
|
|
15780
|
+
"type": "string"
|
|
15781
|
+
},
|
|
15782
|
+
"title": {
|
|
15783
|
+
"type": "string"
|
|
15784
|
+
}
|
|
15785
|
+
},
|
|
15786
|
+
"required": [
|
|
15787
|
+
"kind",
|
|
15788
|
+
"id",
|
|
15789
|
+
"title"
|
|
15790
|
+
],
|
|
15791
|
+
"additionalProperties": {}
|
|
15792
|
+
}
|
|
15793
|
+
},
|
|
15770
15794
|
"inheritedScope": {
|
|
15771
15795
|
"description": "The scope the check inherits: its part's declaration, else its layer's. Absent when neither declared one: unknown, not live or physical",
|
|
15772
15796
|
"type": "object",
|
|
@@ -18974,6 +18998,12 @@
|
|
|
18974
18998
|
],
|
|
18975
18999
|
"additionalProperties": {}
|
|
18976
19000
|
},
|
|
19001
|
+
"attachments": {
|
|
19002
|
+
"description": "Files attached to the run itself, in its layers, not to a check. Absent when there are none",
|
|
19003
|
+
"type": "integer",
|
|
19004
|
+
"exclusiveMinimum": 0,
|
|
19005
|
+
"maximum": 9007199254740991
|
|
19006
|
+
},
|
|
18977
19007
|
"artifactTags": {
|
|
18978
19008
|
"description": "The normalized tags of the attachments of its results, sorted and distinct. Absent when none is tagged, and in reports of older servers",
|
|
18979
19009
|
"type": "array",
|
|
@@ -19928,6 +19958,12 @@
|
|
|
19928
19958
|
],
|
|
19929
19959
|
"additionalProperties": {}
|
|
19930
19960
|
},
|
|
19961
|
+
"attachments": {
|
|
19962
|
+
"description": "Files attached to the run itself, in its layers, not to a check. Absent when there are none",
|
|
19963
|
+
"type": "integer",
|
|
19964
|
+
"exclusiveMinimum": 0,
|
|
19965
|
+
"maximum": 9007199254740991
|
|
19966
|
+
},
|
|
19931
19967
|
"artifactTags": {
|
|
19932
19968
|
"description": "The normalized tags of the attachments of its results, sorted and distinct. Absent when none is tagged, and in reports of older servers",
|
|
19933
19969
|
"type": "array",
|
|
@@ -20188,6 +20224,85 @@
|
|
|
20188
20224
|
],
|
|
20189
20225
|
"additionalProperties": {}
|
|
20190
20226
|
}
|
|
20227
|
+
},
|
|
20228
|
+
"layers": {
|
|
20229
|
+
"description": "Every layer of the run, oldest first. Absent from an older server",
|
|
20230
|
+
"type": "array",
|
|
20231
|
+
"items": {
|
|
20232
|
+
"type": "object",
|
|
20233
|
+
"properties": {
|
|
20234
|
+
"id": {
|
|
20235
|
+
"type": "string",
|
|
20236
|
+
"description": "The layer id; read its files with `/api/reports/evidence/detail/{backend}/{id}`"
|
|
20237
|
+
},
|
|
20238
|
+
"backend": {
|
|
20239
|
+
"type": "string"
|
|
20240
|
+
},
|
|
20241
|
+
"producer": {
|
|
20242
|
+
"type": "string"
|
|
20243
|
+
},
|
|
20244
|
+
"environment": {
|
|
20245
|
+
"type": [
|
|
20246
|
+
"string",
|
|
20247
|
+
"null"
|
|
20248
|
+
]
|
|
20249
|
+
},
|
|
20250
|
+
"recordedAtMs": {
|
|
20251
|
+
"type": "number"
|
|
20252
|
+
},
|
|
20253
|
+
"checks": {
|
|
20254
|
+
"type": "integer",
|
|
20255
|
+
"minimum": 0,
|
|
20256
|
+
"maximum": 9007199254740991,
|
|
20257
|
+
"description": "How many checks the layer recorded. Zero means evidence only, not a pass"
|
|
20258
|
+
},
|
|
20259
|
+
"attachments": {
|
|
20260
|
+
"type": "integer",
|
|
20261
|
+
"minimum": 0,
|
|
20262
|
+
"maximum": 9007199254740991,
|
|
20263
|
+
"description": "Files the layer carries as a whole, not those of its checks"
|
|
20264
|
+
},
|
|
20265
|
+
"videos": {
|
|
20266
|
+
"type": "object",
|
|
20267
|
+
"properties": {
|
|
20268
|
+
"references": {
|
|
20269
|
+
"type": "integer",
|
|
20270
|
+
"minimum": 0,
|
|
20271
|
+
"maximum": 9007199254740991,
|
|
20272
|
+
"description": "Every reference to a video; identical bytes referenced twice count twice"
|
|
20273
|
+
},
|
|
20274
|
+
"recordings": {
|
|
20275
|
+
"type": "integer",
|
|
20276
|
+
"minimum": 0,
|
|
20277
|
+
"maximum": 9007199254740991,
|
|
20278
|
+
"description": "Distinct playable recordings"
|
|
20279
|
+
},
|
|
20280
|
+
"unavailable": {
|
|
20281
|
+
"type": "integer",
|
|
20282
|
+
"minimum": 0,
|
|
20283
|
+
"maximum": 9007199254740991,
|
|
20284
|
+
"description": "References that name no bytes"
|
|
20285
|
+
}
|
|
20286
|
+
},
|
|
20287
|
+
"required": [
|
|
20288
|
+
"references",
|
|
20289
|
+
"recordings",
|
|
20290
|
+
"unavailable"
|
|
20291
|
+
],
|
|
20292
|
+
"additionalProperties": {}
|
|
20293
|
+
}
|
|
20294
|
+
},
|
|
20295
|
+
"required": [
|
|
20296
|
+
"id",
|
|
20297
|
+
"backend",
|
|
20298
|
+
"producer",
|
|
20299
|
+
"environment",
|
|
20300
|
+
"recordedAtMs",
|
|
20301
|
+
"checks",
|
|
20302
|
+
"attachments"
|
|
20303
|
+
],
|
|
20304
|
+
"additionalProperties": {}
|
|
20305
|
+
}
|
|
20191
20306
|
}
|
|
20192
20307
|
},
|
|
20193
20308
|
"required": [
|
|
@@ -4,9 +4,9 @@ import { effectiveScope } from '@crvouga/atlas/reports/evidence-scope';
|
|
|
4
4
|
import type { LayerHeader, LayerRow } from '@crvouga/atlas/reports/evidence-types';
|
|
5
5
|
import { contentTypeOf } from '@crvouga/atlas/reports/media-types';
|
|
6
6
|
import type { Annotation, Attachment, Attempt, Step } from '@crvouga/atlas/schema';
|
|
7
|
-
import { Accordion, Alert, Anchor, Box, Code, Flex, Image, List, Stack, Text, Title } from '@mantine/core';
|
|
7
|
+
import { Accordion, Alert, Anchor, Badge, Box, Code, Flex, Image, List, Stack, Text, Title } from '@mantine/core';
|
|
8
8
|
import { useQuery } from '@tanstack/react-query';
|
|
9
|
-
import { useMemo, type ReactNode } from 'react';
|
|
9
|
+
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
|
10
10
|
import { ArtifactTagList, ArtifactTagsProvider, TagFilterBar, useFilteredAttachments } from './artifact-tags';
|
|
11
11
|
import { reportAssetUrl, reportJson, reportText } from './api';
|
|
12
12
|
import { ScopeNotice, ScopeProvider, useScope } from './evidence-scope';
|
|
@@ -35,6 +35,23 @@ export type ArtifactRenderer = {
|
|
|
35
35
|
const mediaType = (attachment: Attachment) =>
|
|
36
36
|
(attachment.contentType ?? contentTypeOf(attachment.name)).split(';')[0]!.trim().toLowerCase();
|
|
37
37
|
const label = (attachment: Attachment) => attachment.description ?? attachment.name;
|
|
38
|
+
/** A video that plays inline; when the file cannot be loaded or this browser cannot play it, it says so and offers the file, instead of an empty player. */
|
|
39
|
+
function VideoPlayer({ attachment, url }: { attachment: Attachment; url: string }) {
|
|
40
|
+
const [failed, setFailed] = useState(false);
|
|
41
|
+
if (failed)
|
|
42
|
+
return (
|
|
43
|
+
<Alert role="alert" color="orange" title="This video cannot be played">
|
|
44
|
+
<Text component="div">
|
|
45
|
+
{label(attachment)} could not be loaded, or this browser cannot play its format. Nothing is wrong with the report: the file may be missing from the
|
|
46
|
+
store or in an unsupported codec.{' '}
|
|
47
|
+
<Anchor href={url} download={attachment.name} target="_blank" rel="noreferrer">
|
|
48
|
+
Download {attachment.name}
|
|
49
|
+
</Anchor>
|
|
50
|
+
</Text>
|
|
51
|
+
</Alert>
|
|
52
|
+
);
|
|
53
|
+
return <Box src={url} controls preload="metadata" aria-label={label(attachment)} component="video" maw="100%" w="100%" onError={() => setFailed(true)} />;
|
|
54
|
+
}
|
|
38
55
|
function TextValue({ attachment, path }: { attachment: Attachment; path: string | null }) {
|
|
39
56
|
const stored = useQuery({
|
|
40
57
|
queryKey: ['artifact-text', path ?? ''],
|
|
@@ -92,8 +109,7 @@ export const artifactRenderers: ArtifactRenderer[] = [
|
|
|
92
109
|
id: 'video',
|
|
93
110
|
wide: true,
|
|
94
111
|
matches: (_, type) => type.startsWith('video/'),
|
|
95
|
-
render: (a, { url }) =>
|
|
96
|
-
url ? <Box src={url} controls preload="metadata" aria-label={label(a)} component="video" maw="100%" w="100%" /> : null
|
|
112
|
+
render: (a, { url }) => (url ? <VideoPlayer attachment={a} url={url} /> : null)
|
|
97
113
|
},
|
|
98
114
|
{
|
|
99
115
|
id: 'audio',
|
|
@@ -130,7 +146,13 @@ function sizeOf(bytes: number | undefined) {
|
|
|
130
146
|
if (bytes < 1024) return `${bytes} B`;
|
|
131
147
|
return bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(0)} KiB` : `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
|
|
132
148
|
}
|
|
133
|
-
|
|
149
|
+
/**
|
|
150
|
+
* One attachment as a card: it plays or shows itself when it can, and always says what it is (name, role, type, size,
|
|
151
|
+
* duration, tags) and how to get it. A file that has no address, has an address that is not a web address, or that
|
|
152
|
+
* nothing can preview says so in words. Pass `link` for a control that opens this one artifact, `note` for how it
|
|
153
|
+
* relates to the rest of the report, and `selected` to mark the one a link pointed at (it scrolls into view).
|
|
154
|
+
*/
|
|
155
|
+
export function ArtifactCard({ attachment, layer, link, note, selected }: { attachment: Attachment; layer: string; link?: ReactNode; note?: ReactNode; selected?: boolean }) {
|
|
134
156
|
const type = mediaType(attachment);
|
|
135
157
|
const path = attachment.bundleKey
|
|
136
158
|
? `/layers/${encodeURIComponent(layer)}/blobs/${attachment.bundleKey.split('/').map(encodeURIComponent).join('/')}`
|
|
@@ -138,17 +160,28 @@ function Artifact({ attachment, layer }: { attachment: Attachment; layer: string
|
|
|
138
160
|
const url = path ? reportAssetUrl(path) : safeUri(attachment.uri);
|
|
139
161
|
const renderer = artifactRenderers.find((candidate) => candidate.matches(attachment, type));
|
|
140
162
|
const body = renderer?.render(attachment, { url, path, type });
|
|
163
|
+
const item = useRef<HTMLLIElement>(null);
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (selected) item.current?.scrollIntoView?.({ block: 'center' });
|
|
166
|
+
}, [selected]);
|
|
141
167
|
const scope = effectiveScope(attachment.scope, useScope());
|
|
142
168
|
// A recording says what it shows before it is played; evidence that declared a scope of its own says so too.
|
|
143
169
|
const scoped = isVideoAttachment(attachment) || attachment.scope !== undefined;
|
|
144
170
|
return (
|
|
145
|
-
<List.Item
|
|
171
|
+
<List.Item
|
|
172
|
+
ref={item}
|
|
173
|
+
data-wide={renderer?.wide || undefined}
|
|
174
|
+
data-role={attachment.role ?? roleOfType(type)}
|
|
175
|
+
data-selected={selected || undefined}
|
|
176
|
+
{...(selected ? { bg: 'blue.0', p: 'sm', bd: '2px solid var(--mantine-color-blue-6)' } : {})}
|
|
177
|
+
>
|
|
146
178
|
{scoped && <ScopeNotice scope={scope} name={attachment.name} />}
|
|
147
179
|
{body}
|
|
148
180
|
<Box component="figcaption">
|
|
149
181
|
<Text span inherit fw={600}>
|
|
150
182
|
{attachment.name}
|
|
151
183
|
</Text>
|
|
184
|
+
{selected && <Badge variant="light">Selected</Badge>}
|
|
152
185
|
{attachment.role && (
|
|
153
186
|
<Text span inherit>
|
|
154
187
|
{attachment.role}
|
|
@@ -162,12 +195,21 @@ function Artifact({ attachment, layer }: { attachment: Attachment; layer: string
|
|
|
162
195
|
{body ? 'Download' : 'Open'}
|
|
163
196
|
</Anchor>
|
|
164
197
|
)}
|
|
165
|
-
{
|
|
166
|
-
<Text span inherit>
|
|
167
|
-
No file to show
|
|
168
|
-
</Text>
|
|
169
|
-
)}
|
|
198
|
+
{link}
|
|
170
199
|
</Box>
|
|
200
|
+
{!url && attachment.text === undefined && (
|
|
201
|
+
<Alert role={selected ? 'alert' : undefined} color="orange" data-unavailable my="xs">
|
|
202
|
+
{attachment.uri
|
|
203
|
+
? 'No file to show. This file is only given by an address that is not a web address, so Atlas will not follow it. Ask the producer for a web address or to upload the file.'
|
|
204
|
+
: 'No file to show. The report lists this file, but it was never stored, so it cannot be shown or downloaded. Publish it again with the file attached.'}
|
|
205
|
+
</Alert>
|
|
206
|
+
)}
|
|
207
|
+
{url && !renderer && (
|
|
208
|
+
<Text component="div" c="dimmed" data-no-preview>
|
|
209
|
+
Atlas has no preview for this kind of file. Open or download it to inspect it.
|
|
210
|
+
</Text>
|
|
211
|
+
)}
|
|
212
|
+
{note}
|
|
171
213
|
<ArtifactTagList attachment={attachment} />
|
|
172
214
|
</List.Item>
|
|
173
215
|
);
|
|
@@ -182,7 +224,7 @@ export function Attachments({ attachments: all, layer }: { attachments: readonly
|
|
|
182
224
|
</Title>
|
|
183
225
|
<List spacing="xs" listStyleType="none">
|
|
184
226
|
{attachments.map((attachment, index) => (
|
|
185
|
-
<
|
|
227
|
+
<ArtifactCard key={`${attachment.id ?? attachment.name}:${index}`} attachment={attachment} layer={layer} />
|
|
186
228
|
))}
|
|
187
229
|
</List>
|
|
188
230
|
</Box>
|
|
@@ -142,10 +142,10 @@ export function Explorer() {
|
|
|
142
142
|
<Title order={1} mb="sm">
|
|
143
143
|
{projectTitle()}
|
|
144
144
|
</Title>
|
|
145
|
-
{model && model.tests.length > 0 && (
|
|
145
|
+
{model && (model.tests.length > 0 || model.summary.runs > 0) && (
|
|
146
146
|
<Tabs
|
|
147
147
|
value={search.tab ?? 'tests'}
|
|
148
|
-
onChange={(value) => go({ tab: value === 'runs' || value === 'artifacts' ? value : undefined, test: undefined, openRun: undefined, layer: undefined })}
|
|
148
|
+
onChange={(value) => go({ tab: value === 'runs' || value === 'artifacts' ? value : undefined, test: undefined, openRun: undefined, layer: undefined, artifact: undefined })}
|
|
149
149
|
>
|
|
150
150
|
<Tabs.List aria-label="Explorer view">
|
|
151
151
|
<Tabs.Tab
|
|
@@ -212,7 +212,12 @@ export function Explorer() {
|
|
|
212
212
|
Loading tests…
|
|
213
213
|
</Text>
|
|
214
214
|
)}
|
|
215
|
-
{empty &&
|
|
215
|
+
{empty && model && model.summary.runs > 0 && search.tab !== 'runs' && (
|
|
216
|
+
<Text component="div" c="dimmed">
|
|
217
|
+
No test results recorded, but {model.summary.runs === 1 ? '1 run carries' : `${model.summary.runs} runs carry`} files. Open the Runs tab to see them.
|
|
218
|
+
</Text>
|
|
219
|
+
)}
|
|
220
|
+
{empty && !kinds.length && !(model && model.summary.runs > 0) && (
|
|
216
221
|
<Text component="div" c="dimmed">
|
|
217
222
|
No reports yet. Publish tests and specs to this project to populate its report.
|
|
218
223
|
</Text>
|
|
@@ -223,13 +228,12 @@ export function Explorer() {
|
|
|
223
228
|
</Text>
|
|
224
229
|
)}
|
|
225
230
|
{model &&
|
|
226
|
-
!empty &&
|
|
227
231
|
listing &&
|
|
228
232
|
(search.tab === 'runs' ? (
|
|
229
233
|
<RunsView />
|
|
230
234
|
) : search.tab === 'artifacts' ? (
|
|
231
235
|
<ArtifactsView />
|
|
232
|
-
) : (
|
|
236
|
+
) : empty ? null : (
|
|
233
237
|
<>
|
|
234
238
|
<InsightStrip model={model} scope={listing.scope} search={search} />
|
|
235
239
|
<Toolbar
|