archsight 0.4.2 → 0.5.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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +4 -0
  3. data/docs/configuration.md +5 -0
  4. data/docs/diagram.md +69 -0
  5. data/docs/index.md.erb +1 -0
  6. data/docs/modeling.md +3 -1
  7. data/docs/pdf-export.md +118 -0
  8. data/lib/archsight/analysis/typst_renderer.rb +25 -78
  9. data/lib/archsight/cli.rb +39 -1
  10. data/lib/archsight/database.rb +4 -3
  11. data/lib/archsight/diagram/cli.rb +11 -1
  12. data/lib/archsight/diagram/errors.rb +1 -0
  13. data/lib/archsight/diagram/graph/attributes.rb +3 -1
  14. data/lib/archsight/diagram/graph/node.rb +18 -1
  15. data/lib/archsight/diagram/includes.rb +168 -0
  16. data/lib/archsight/diagram/layout/expander.rb +75 -3
  17. data/lib/archsight/diagram/layout/rank_arranger.rb +3 -1
  18. data/lib/archsight/diagram/parser/ast.rb +5 -0
  19. data/lib/archsight/diagram/parser/table_layout.rb +2 -0
  20. data/lib/archsight/diagram/parser.rb +58 -16
  21. data/lib/archsight/diagram.rb +20 -4
  22. data/lib/archsight/export/confluence/storage.rb +6 -1
  23. data/lib/archsight/helpers/diagram_blocks.rb +34 -19
  24. data/lib/archsight/helpers/diagram_includes.rb +67 -0
  25. data/lib/archsight/helpers.rb +1 -0
  26. data/lib/archsight/linter.rb +10 -2
  27. data/lib/archsight/markdown.rb +17 -1
  28. data/lib/archsight/pdf/cache.rb +41 -0
  29. data/lib/archsight/pdf/compiler.rb +69 -0
  30. data/lib/archsight/pdf/dependency_graph.rb +127 -0
  31. data/lib/archsight/pdf/document.rb +227 -0
  32. data/lib/archsight/pdf/embeds.rb +134 -0
  33. data/lib/archsight/pdf/images.rb +94 -0
  34. data/lib/archsight/pdf/markdown.rb +217 -0
  35. data/lib/archsight/pdf/remote_image.rb +122 -0
  36. data/lib/archsight/pdf/resource_sheet.rb +129 -0
  37. data/lib/archsight/pdf/sample_document.rb +67 -0
  38. data/lib/archsight/pdf/tables.rb +72 -0
  39. data/lib/archsight/pdf/theme.rb +125 -0
  40. data/lib/archsight/pdf/typst/archsight.typ +152 -0
  41. data/lib/archsight/pdf/typst.rb +45 -0
  42. data/lib/archsight/pdf.rb +30 -0
  43. data/lib/archsight/references.rb +4 -3
  44. data/lib/archsight/resources/application_component.rb +10 -0
  45. data/lib/archsight/resources/application_service.rb +9 -0
  46. data/lib/archsight/resources/config.rb +147 -0
  47. data/lib/archsight/resources/technology_service.rb +9 -0
  48. data/lib/archsight/version.rb +1 -1
  49. data/lib/archsight/web/api/json_helpers.rb +3 -2
  50. data/lib/archsight/web/api/openapi/spec.yaml +239 -2
  51. data/lib/archsight/web/api/page_helpers.rb +1 -1
  52. data/lib/archsight/web/api/pdf_helpers.rb +46 -0
  53. data/lib/archsight/web/api/routes.rb +34 -3
  54. data/lib/archsight/web/application.rb +2 -1
  55. data/lib/archsight/web/public/vue/DocPage-D3QtnrmO.js +1 -0
  56. data/lib/archsight/web/public/vue/EditorPage-lZyG6dVt.js +35 -0
  57. data/lib/archsight/web/public/vue/ErrorPage-BhdYGZJk.js +2 -0
  58. data/lib/archsight/web/public/vue/GraphView-CoqAjFdR.js +1 -0
  59. data/lib/archsight/web/public/vue/HomePage-B7SaXVeb.js +2 -0
  60. data/lib/archsight/web/public/vue/{InstanceRouter-Ca_C8izS.css → InstanceRouter-BCKbW-Wp.css} +1 -1
  61. data/lib/archsight/web/public/vue/InstanceRouter-DJJn9fhP.js +1 -0
  62. data/lib/archsight/web/public/vue/KindList-UqMajqbf.js +1 -0
  63. data/lib/archsight/web/public/vue/PageView-CirIXjW6.js +1 -0
  64. data/lib/archsight/web/public/vue/QueryError-BIJH6gQC.js +1 -0
  65. data/lib/archsight/web/public/vue/ResourceList-761vG1Jw.js +2 -0
  66. data/lib/archsight/web/public/vue/SearchResults-BPHa6yhK.js +1 -0
  67. data/lib/archsight/web/public/vue/WikiPage-Bi7dzY2h.js +3 -0
  68. data/lib/archsight/web/public/vue/{WikiPage-TpmM1VAF.css → WikiPage-CuUi4C8v.css} +1 -1
  69. data/lib/archsight/web/public/vue/{index-yiZbbvAs.js → index-BF4zUlWZ.js} +2 -2
  70. data/lib/archsight/web/public/vue/index-BO59Fbpf.css +1 -0
  71. data/lib/archsight/web/public/vue.html +2 -2
  72. metadata +37 -17
  73. data/lib/archsight/web/public/vue/DocPage-C8Ysway-.js +0 -1
  74. data/lib/archsight/web/public/vue/EditorPage-pr2MR6_b.js +0 -35
  75. data/lib/archsight/web/public/vue/ErrorPage-FAkpW-hn.js +0 -2
  76. data/lib/archsight/web/public/vue/GraphView-liQJzFyo.js +0 -1
  77. data/lib/archsight/web/public/vue/HomePage-DYpzSXG8.js +0 -2
  78. data/lib/archsight/web/public/vue/InstanceRouter-BdkBTmGO.js +0 -1
  79. data/lib/archsight/web/public/vue/KindList-CrgI4qDb.js +0 -1
  80. data/lib/archsight/web/public/vue/PageView-BIxT_t49.js +0 -1
  81. data/lib/archsight/web/public/vue/QueryError-ChdyV8B1.js +0 -1
  82. data/lib/archsight/web/public/vue/ResourceList-DdtaHY4k.js +0 -2
  83. data/lib/archsight/web/public/vue/SearchResults-BYRAcK_o.js +0 -1
  84. data/lib/archsight/web/public/vue/WikiPage-CgvRN6R2.js +0 -3
  85. data/lib/archsight/web/public/vue/index-BLSE5DSv.css +0 -1
@@ -35,6 +35,12 @@ class Archsight::Resources::ApplicationService < Archsight::Resources::Base
35
35
  the other service's components listed again; a service that many others use is modeled once. A service whose only component has the same name is a smell. See
36
36
  the modeling guide, "Choosing the Kind".
37
37
 
38
+ ## Teams
39
+
40
+ A service is `maintainedBy` the team (`BusinessActor`) that builds and changes it and `operatedBy` the team
41
+ (`BusinessActor` or `BusinessRole`) that runs it in production. Build and run are often different teams, which is
42
+ the point of writing both. `servedBy` stays "who uses it". Both are optional.
43
+
38
44
  ## Security and risk modelling
39
45
 
40
46
  A service is an asset at risk (`asset/*` profile, protection needs) that loss events `affects`, a trigger target
@@ -230,6 +236,9 @@ class Archsight::Resources::ApplicationService < Archsight::Resources::Base
230
236
  relation :dependsOn, :applicationServices, :ApplicationService
231
237
  relation :servedBy, :businessActors, :BusinessActor
232
238
  relation :servedBy, :technologyServices, :TechnologyService
239
+ relation :maintainedBy, :businessActors, :BusinessActor
240
+ relation :operatedBy, :businessActors, :BusinessActor
241
+ relation :operatedBy, :businessRoles, :BusinessRole
233
242
  relation :realizes, :motivationConstraints, :MotivationConstraint
234
243
  relation :realizes, :motivationRequirements, :MotivationRequirement
235
244
  relation :partiallyRealizes, :motivationRequirements, :MotivationRequirement
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Config holds settings of a feature of the tool, kept with the model so they are shared and versioned like it
4
+ class Archsight::Resources::Config < Archsight::Resources::Base
5
+ include_annotations :architecture
6
+
7
+ description <<~MD
8
+ Holds the settings of a feature of the tool, such as the look of the PDF export.
9
+
10
+ ## Definition
11
+
12
+ A Config is a tool-specific resource type. It carries no architecture, only settings, so that everything that
13
+ should look or behave the same is written once and shared through the resources repository instead of being
14
+ repeated in every page. It is not the per-user configuration file (tokens, URLs of other systems): that stays in
15
+ `~/.config/archsight/archsight.yaml`.
16
+
17
+ ## Type
18
+
19
+ `config/type` says which feature the settings are for; the feature looks up its own Configs by type, so every
20
+ feature can have a Config named `default`. The settings of a feature use the annotations of its own prefix.
21
+
22
+ | `config/type` | Settings | Used by |
23
+ |---------------|----------|---------|
24
+ | `pdf` | `pdf/*`: colours, fonts, logo, header and footer | [PDF export](/doc/pdf-export) |
25
+
26
+ A document picks a different Config of the type with the tag `pdf:template:<name>`; without a tag the one named
27
+ `default` is used, and without any the export has a neutral built-in look.
28
+
29
+ ## PDF settings
30
+
31
+ - **Colours** (`pdf/color-*`) are hex values (`#0b3d91`). `primary` is used for headings, the cover band and
32
+ table headers, `accent` for rules and links.
33
+ - **Fonts** (`pdf/font-*`) are family names. Fonts that are not installed fall back to the built-in ones; put
34
+ font files in a directory of the resources and name it in `pdf/font-dir` to ship them with the model.
35
+ - **Logos** (`pdf/logo`, `pdf/logo-light`) are image files (svg, png) of the resources directory, given
36
+ relative to it. `logo` goes on white (page header), `logo-light` on the coloured cover band.
37
+ - **Texts** (`pdf/footer-text`, `pdf/classification`) appear on every page.
38
+ MD
39
+
40
+ icon "settings"
41
+ layer "other"
42
+
43
+ TYPE_FORMAT = /\A[a-z][a-z0-9-]*\z/
44
+
45
+ annotation "config/type",
46
+ description: "The feature these settings are for (pdf). The feature looks up its Configs by this type",
47
+ title: "Type",
48
+ filter: :word,
49
+ summary: true,
50
+ validator: ->(value) { Archsight::Resources::Config.pattern_error(value, TYPE_FORMAT, "a lowercase word such as pdf") }
51
+
52
+ COLOR = /\A#(\h{3}|\h{6})\z/
53
+ FONT_NAME = /\A[\p{L}\p{N} ._+-]{1,80}\z/
54
+ LENGTH = /\A\d+(\.\d+)?(mm|cm|pt|in)\z/
55
+
56
+ # Colour annotations: key suffix, title, role
57
+ COLORS = {
58
+ "primary" => ["Primary colour", "headings, cover band, table headers"],
59
+ "accent" => ["Accent colour", "rules and links"],
60
+ "text" => ["Text colour", "body text"],
61
+ "muted" => ["Muted colour", "secondary text, header and footer"],
62
+ "ok" => ["Positive colour", "good status"],
63
+ "warn" => ["Warning colour", "warning status"],
64
+ "danger" => ["Danger colour", "bad status and the classification marking"]
65
+ }.freeze
66
+
67
+ COLORS.each do |key, (title, role)|
68
+ annotation "pdf/color-#{key}",
69
+ description: "#{title} as hex value (#rrggbb), used for #{role}",
70
+ title: title,
71
+ validator: ->(value) { Archsight::Resources::Config.pattern_error(value, COLOR, "a hex colour such as #0b3d91") },
72
+ sidebar: false
73
+ end
74
+
75
+ %w[body heading mono].each do |role|
76
+ annotation "pdf/font-#{role}",
77
+ description: "Font family of the #{role} text",
78
+ title: "#{role.capitalize} font",
79
+ validator: ->(value) { Archsight::Resources::Config.pattern_error(value, FONT_NAME, "a font family name") },
80
+ sidebar: false
81
+ end
82
+
83
+ annotation "pdf/font-dir",
84
+ description: "Directory with font files (ttf, otf), relative to the resources directory",
85
+ title: "Font directory",
86
+ sidebar: false
87
+
88
+ annotation "pdf/logo",
89
+ description: "Logo for the page header (svg or png), path relative to the resources directory",
90
+ title: "Logo",
91
+ validator: ->(value) { Archsight::Resources::Config.logo_error(value) },
92
+ sidebar: false
93
+ annotation "pdf/logo-light",
94
+ description: "Logo for the coloured cover band (svg or png), path relative to the resources directory",
95
+ title: "Logo (light)",
96
+ validator: ->(value) { Archsight::Resources::Config.logo_error(value) },
97
+ sidebar: false
98
+
99
+ annotation "pdf/footer-text",
100
+ description: "Text in the footer of every page (centre)",
101
+ title: "Footer text",
102
+ sidebar: false
103
+ annotation "pdf/classification",
104
+ description: "Marking shown in the footer of every page, e.g. Internal. A document can override it with " \
105
+ "the page property Classification",
106
+ title: "Classification",
107
+ sidebar: false
108
+ annotation "pdf/header-meta",
109
+ description: "Comma-separated parts of the header line: id, version, date, owner (default: all)",
110
+ title: "Header line",
111
+ sidebar: false,
112
+ validator: ->(value) { Archsight::Resources::Config.header_meta_error(value) }
113
+ annotation "pdf/lang",
114
+ description: "Language of the fixed texts (page, contents, ...)",
115
+ title: "Language",
116
+ enum: %w[en de],
117
+ sidebar: false
118
+ annotation "pdf/paper",
119
+ description: "Paper size",
120
+ title: "Paper",
121
+ enum: %w[a4 a5 us-letter],
122
+ sidebar: false
123
+ annotation "pdf/margin",
124
+ description: "Horizontal page margin as length (e.g. 16mm)",
125
+ title: "Margin",
126
+ validator: ->(value) { Archsight::Resources::Config.pattern_error(value, LENGTH, "a length such as 16mm") },
127
+ sidebar: false
128
+
129
+ HEADER_PARTS = %w[id version date owner].freeze
130
+
131
+ # Validator helpers: nil if the value is fine, else the message
132
+ def self.pattern_error(value, pattern, expected)
133
+ value.to_s.match?(pattern) ? nil : "expected #{expected}"
134
+ end
135
+
136
+ def self.logo_error(value)
137
+ ext = File.extname(value.to_s).downcase
138
+ return nil if %w[.svg .png].include?(ext) && Archsight::Assets.normalize(value.to_s)
139
+
140
+ "expected a path of an svg or png file relative to the resources directory"
141
+ end
142
+
143
+ def self.header_meta_error(value)
144
+ unknown = value.to_s.split(",").map(&:strip) - HEADER_PARTS
145
+ unknown.empty? ? nil : "unknown part #{unknown.join(", ")}; use #{HEADER_PARTS.join(", ")}"
146
+ end
147
+ end
@@ -50,6 +50,12 @@ class Archsight::Resources::TechnologyService < Archsight::Resources::Base
50
50
  "servedBy" from ApplicationComponent on its page. An ApplicationService that is `servedBy` the service uses it as a
51
51
  whole; list a cluster there only when the service's own components run in it.
52
52
 
53
+ ## Teams
54
+
55
+ A service is `maintainedBy` the team (`BusinessActor`) that builds and changes it and `operatedBy` the team
56
+ (`BusinessActor` or `BusinessRole`) that runs it in production. Build and run are often different teams, which is
57
+ the point of writing both. `servedBy` stays "who uses it". Both are optional.
58
+
53
59
  ## Provisioning
54
60
 
55
61
  A service that software creates (clusters built by an infrastructure-as-code operator, a database cluster
@@ -75,6 +81,9 @@ class Archsight::Resources::TechnologyService < Archsight::Resources::Base
75
81
  relation :suppliedBy, :technologyComponents, :TechnologySystemSoftware
76
82
  relation :specializes, :technologyServices, :TechnologyService
77
83
  relation :servedBy, :businessActors, :BusinessActor
84
+ relation :maintainedBy, :businessActors, :BusinessActor
85
+ relation :operatedBy, :businessActors, :BusinessActor
86
+ relation :operatedBy, :businessRoles, :BusinessRole
78
87
  relation :locatedAt, :technologyFacilities, :TechnologyFacility
79
88
  relation :realizedBy, :technologyEquipment, :TechnologyEquipment
80
89
  relation :realizes, :motivationRequirements, :MotivationRequirement
@@ -4,5 +4,5 @@
4
4
  # Do not edit manually.
5
5
 
6
6
  module Archsight
7
- VERSION = "0.4.2"
7
+ VERSION = "0.5.0"
8
8
  end
@@ -106,7 +106,8 @@ module Archsight::Web::API::JsonHelpers
106
106
  source = annotations[DIAGRAM_ANNOTATION_KEY]
107
107
  return unless source.is_a?(String) && !source.strip.empty?
108
108
 
109
- Archsight::Helpers::DiagramBlocks.render_diagram(source, resolver: Archsight::Helpers::ResourceResolver.new(db))
109
+ Archsight::Helpers::DiagramBlocks.render_diagram(source, resolver: Archsight::Helpers::ResourceResolver.new(db),
110
+ includes: Archsight::Helpers::DiagramIncludes.for(db))
110
111
  end
111
112
 
112
113
  # The values of markdown annotations are rendered to HTML: architecture/description and every annotation the
@@ -190,7 +191,7 @@ module Archsight::Web::API::JsonHelpers
190
191
  title: annotation.title,
191
192
  description: annotation.description,
192
193
  filter_type: annotation.filter.to_s,
193
- values: values
194
+ values: values.map { |value, count| { value: value, count: count } }
194
195
  }
195
196
  end
196
197
  end
@@ -374,6 +374,234 @@ paths:
374
374
  "404":
375
375
  description: Unknown colour
376
376
 
377
+ "/api/v1/kinds/Config/instances/{name}/preview":
378
+ get:
379
+ summary: Preview the look of a PDF Config
380
+ description: A fixed sample document (cover page and a page with headings, list,
381
+ quote, table with status badges and code) in the look of one `Config` of type
382
+ `pdf`, typeset by Typst like every PDF export. Served inline, for showing it
383
+ in a page. Only the Config shapes the result, so a changed colour, font or logo
384
+ is visible at once. The response carries an `ETag` (fingerprint of the document
385
+ and the files it is made of) and answers `304` for a matching `If-None-Match`.
386
+ operationId: getConfigPreview
387
+ tags:
388
+ - PDF
389
+ parameters:
390
+ - name: name
391
+ in: path
392
+ required: true
393
+ description: Name of the Config, e.g. `Config:PDF:default`
394
+ schema:
395
+ type: string
396
+ - name: part
397
+ in: query
398
+ required: false
399
+ description: "`cover` or `page` for a document of just that page (the viewer needs no navigation); without it both pages"
400
+ schema:
401
+ type: string
402
+ enum:
403
+ - cover
404
+ - page
405
+ - name: If-None-Match
406
+ in: header
407
+ required: false
408
+ description: The `ETag` of a copy the client has; a match answers `304`
409
+ schema:
410
+ type: string
411
+ responses:
412
+ '200':
413
+ description: The PDF
414
+ headers:
415
+ ETag:
416
+ schema:
417
+ type: string
418
+ Content-Disposition:
419
+ description: '`inline; filename="preview-<name>.pdf"`'
420
+ schema:
421
+ type: string
422
+ content:
423
+ application/pdf:
424
+ schema:
425
+ type: string
426
+ format: binary
427
+ '304':
428
+ description: The client's copy is current
429
+ '404':
430
+ description: No such Config
431
+ content:
432
+ application/json:
433
+ schema:
434
+ "$ref": "#/components/schemas/Error"
435
+ '422':
436
+ description: The Config is not of type `pdf`, or the sample could not be typeset
437
+ (message of typst)
438
+ content:
439
+ application/json:
440
+ schema:
441
+ "$ref": "#/components/schemas/Error"
442
+ '503':
443
+ description: typst is not installed
444
+ content:
445
+ application/json:
446
+ schema:
447
+ "$ref": "#/components/schemas/Error"
448
+
449
+ "/api/v1/kinds/{kind}/instances/{name}/pdf":
450
+ get:
451
+ summary: Download a resource as PDF
452
+ description: |
453
+ Returns one resource as a PDF document, laid out by Typst (`typst` must be installed, the container image has it).
454
+
455
+ **What it contains**: a page is exported as written (headings, tables, images, `asd` diagrams as vector graphics,
456
+ `[[links]]` as their label); any other resource becomes a generated document with its description,
457
+ properties, relations and its `architecture/diagram`. Tags steer the layout: `pdf:cover`, `pdf:toc`,
458
+ `pdf:landscape`, `pdf:draft`, `pdf:template:<name>` and `version:<x>`.
459
+
460
+ **Look**: colours, fonts, logo and the texts of header and footer come from the `Config` resource of type `pdf` named
461
+ `default` (or the one `template` / the tag names); without one a neutral look is used.
462
+
463
+ The response carries an `ETag` (a fingerprint of everything the PDF is made from, so a changed page, image,
464
+ diagram or template changes it) and answers `304` for a matching `If-None-Match`.
465
+ operationId: getInstancePdf
466
+ tags:
467
+ - PDF
468
+ parameters:
469
+ - "$ref": "#/components/parameters/kind"
470
+ - "$ref": "#/components/parameters/name"
471
+ - name: template
472
+ in: query
473
+ required: false
474
+ description: Name of a `Config` of type `pdf` to use instead of the tag or `default`
475
+ schema:
476
+ type: string
477
+ - name: draft
478
+ in: query
479
+ required: false
480
+ description: "`1` adds the draft watermark"
481
+ schema:
482
+ type: string
483
+ enum:
484
+ - '1'
485
+ - name: If-None-Match
486
+ in: header
487
+ required: false
488
+ description: The `ETag` of a copy the client has; a match answers `304`
489
+ schema:
490
+ type: string
491
+ responses:
492
+ '200':
493
+ description: The PDF
494
+ headers:
495
+ ETag:
496
+ schema:
497
+ type: string
498
+ Content-Disposition:
499
+ description: '`attachment; filename="<name>.pdf"`'
500
+ schema:
501
+ type: string
502
+ content:
503
+ application/pdf:
504
+ schema:
505
+ type: string
506
+ format: binary
507
+ '304':
508
+ description: The client's copy is current
509
+ '404':
510
+ description: Not found
511
+ content:
512
+ application/json:
513
+ schema:
514
+ "$ref": "#/components/schemas/Error"
515
+ '422':
516
+ description: The document could not be typeset (message of typst)
517
+ content:
518
+ application/json:
519
+ schema:
520
+ "$ref": "#/components/schemas/Error"
521
+ '503':
522
+ description: typst is not installed
523
+ content:
524
+ application/json:
525
+ schema:
526
+ "$ref": "#/components/schemas/Error"
527
+ "/api/v1/pages/{name}/pdf":
528
+ get:
529
+ summary: Download a page as PDF
530
+ description: |
531
+ Returns one page as a PDF document, laid out by Typst (`typst` must be installed, the container image has it).
532
+
533
+ **What it contains**: a page is exported as written (headings, tables, images, `asd` diagrams as vector graphics,
534
+ `[[links]]` as their label); any other resource becomes a generated document with its description,
535
+ properties, relations and its `architecture/diagram`. Tags steer the layout: `pdf:cover`, `pdf:toc`,
536
+ `pdf:landscape`, `pdf:draft`, `pdf:template:<name>` and `version:<x>`.
537
+
538
+ **Look**: colours, fonts, logo and the texts of header and footer come from the `Config` resource of type `pdf` named
539
+ `default` (or the one `template` / the tag names); without one a neutral look is used.
540
+
541
+ The response carries an `ETag` (a fingerprint of everything the PDF is made from, so a changed page, image,
542
+ diagram or template changes it) and answers `304` for a matching `If-None-Match`.
543
+ operationId: getPagePdf
544
+ tags:
545
+ - PDF
546
+ parameters:
547
+ - "$ref": "#/components/parameters/name"
548
+ - name: template
549
+ in: query
550
+ required: false
551
+ description: Name of a `Config` of type `pdf` to use instead of the tag or `default`
552
+ schema:
553
+ type: string
554
+ - name: draft
555
+ in: query
556
+ required: false
557
+ description: "`1` adds the draft watermark"
558
+ schema:
559
+ type: string
560
+ enum:
561
+ - '1'
562
+ - name: If-None-Match
563
+ in: header
564
+ required: false
565
+ description: The `ETag` of a copy the client has; a match answers `304`
566
+ schema:
567
+ type: string
568
+ responses:
569
+ '200':
570
+ description: The PDF
571
+ headers:
572
+ ETag:
573
+ schema:
574
+ type: string
575
+ Content-Disposition:
576
+ description: '`attachment; filename="<name>.pdf"`'
577
+ schema:
578
+ type: string
579
+ content:
580
+ application/pdf:
581
+ schema:
582
+ type: string
583
+ format: binary
584
+ '304':
585
+ description: The client's copy is current
586
+ '404':
587
+ description: Not found
588
+ content:
589
+ application/json:
590
+ schema:
591
+ "$ref": "#/components/schemas/Error"
592
+ '422':
593
+ description: The document could not be typeset (message of typst)
594
+ content:
595
+ application/json:
596
+ schema:
597
+ "$ref": "#/components/schemas/Error"
598
+ '503':
599
+ description: typst is not installed
600
+ content:
601
+ application/json:
602
+ schema:
603
+ "$ref": "#/components/schemas/Error"
604
+
377
605
  /api/v1/assets/{path}:
378
606
  get:
379
607
  summary: Get an asset (image or draw.io diagram)
@@ -1581,9 +1809,16 @@ components:
1581
1809
  description: Filter type (e.g., "select", "boolean")
1582
1810
  values:
1583
1811
  type: array
1812
+ description: Available values with the number of instances using them, most used first
1584
1813
  items:
1585
- type: string
1586
- description: Available values for the filter
1814
+ type: object
1815
+ required: [value, count]
1816
+ properties:
1817
+ value:
1818
+ type: string
1819
+ count:
1820
+ type: integer
1821
+ description: Number of instances with this value
1587
1822
 
1588
1823
  AnalysisResult:
1589
1824
  type: object
@@ -2113,6 +2348,8 @@ tags:
2113
2348
  description: Search and query operations
2114
2349
  - name: Pages
2115
2350
  description: Wiki pages (markdown with frontmatter), the page tree and the home page
2351
+ - name: PDF
2352
+ description: Pages and resources as PDF documents (Typst)
2116
2353
  - name: Assets
2117
2354
  description: Images and draw.io diagrams that markdown embeds
2118
2355
  - name: Macros
@@ -24,7 +24,7 @@ module Archsight::Web::API::PageHelpers
24
24
  next if counts.empty?
25
25
 
26
26
  { key: annotation.key, title: annotation.title, filter_type: annotation.filter.to_s,
27
- values: counts.sort.map { |value, count| { value: value, count: count } } }
27
+ values: counts.sort_by { |value, count| [-count, value.to_s] }.map { |value, count| { value: value, count: count } } }
28
28
  end
29
29
  end
30
30
 
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../pdf"
4
+ require_relative "../../pdf/cache"
5
+ require_relative "../../pdf/sample_document"
6
+
7
+ module Archsight; end
8
+ module Archsight::Web; end
9
+ module Archsight::Web::API; end
10
+
11
+ # Download of a page or resource as PDF
12
+ module Archsight::Web::API::PdfHelpers
13
+ CACHE = Archsight::Pdf::Cache.new
14
+ COMPILER = Archsight::Pdf::Compiler.new
15
+
16
+ # Sends the PDF of `resource`. The ETag is the fingerprint of everything the PDF is made from, so a client (and
17
+ # the server's cache) keeps it exactly as long as the page, its images, diagrams and the template are unchanged.
18
+ # Query: `template` (name of a pdf Config), `draft=1` (watermark).
19
+ def serve_pdf(resource)
20
+ pdf_response do
21
+ Archsight::Pdf.document(db, resource, compiler: COMPILER, template: params[:template].to_s.empty? ? nil : params[:template],
22
+ draft: params[:draft] == "1")
23
+ end
24
+ end
25
+
26
+ # Sends the sample document in the look of a pdf Config, shown in the page instead of downloaded
27
+ def serve_pdf_preview(config)
28
+ pdf_response("inline") { Archsight::Pdf::SampleDocument.new(db, config, part: params[:part], pdf_images: COMPILER.pdf_images?) }
29
+ end
30
+
31
+ # Typesets the document the block builds and sends it; typst problems are answered as JSON errors
32
+ def pdf_response(disposition = "attachment")
33
+ document = yield
34
+ headers "X-Content-Type-Options" => "nosniff"
35
+ cache_control :private, :must_revalidate, max_age: 0
36
+ etag document.fingerprint
37
+ pdf = CACHE.fetch(document.fingerprint) { Archsight::Pdf.compile(document, compiler: COMPILER) }
38
+ content_type "application/pdf"
39
+ headers "Content-Disposition" => %(#{disposition}; filename="#{document.filename}")
40
+ pdf
41
+ rescue Archsight::Pdf::Unavailable => e
42
+ json_error(e.message, status: 503, error_type: "ServiceUnavailable")
43
+ rescue Archsight::Pdf::Error => e
44
+ json_error(e.message, status: 422, error_type: "UnprocessableEntity")
45
+ end
46
+ end
@@ -6,6 +6,7 @@ require_relative "json_helpers"
6
6
  require_relative "page_helpers"
7
7
  require_relative "requirements_helpers"
8
8
  require_relative "analysis_helpers"
9
+ require_relative "pdf_helpers"
9
10
  require_relative "../../assets"
10
11
  require_relative "../../openapi"
11
12
 
@@ -21,6 +22,7 @@ module Archsight::Web::API::Routes
21
22
  helpers Archsight::Web::API::PageHelpers
22
23
  helpers Archsight::Web::API::RequirementsHelpers
23
24
  helpers Archsight::Web::API::AnalysisHelpers
25
+ helpers Archsight::Web::API::PdfHelpers
24
26
 
25
27
  # Rendering is CPU work, previews are typed, not pasted
26
28
  MAX_DIAGRAM_SOURCE = 100_000
@@ -79,6 +81,26 @@ module Archsight::Web::API::Routes
79
81
  .merge(graph_layers: Archsight::GraphvisRenderer.default_graph_layers(instance.class.layer)))
80
82
  end
81
83
 
84
+ # GET /api/v1/kinds/Config/instances/:name/preview - A sample document in the look of a pdf Config (inline PDF)
85
+ get "/api/v1/kinds/Config/instances/:name/preview" do
86
+ config = db.instance_by_kind("Config", params[:name]) || db.instance_by_alias("Config", params[:name])
87
+ json_error("Instance '#{params[:name]}' not found", status: 404, error_type: "NotFound") unless config
88
+ json_error("Config '#{config.name}' is not of type #{Archsight::Pdf::Theme::TYPE}", status: 422, error_type: "UnprocessableEntity") unless config.annotations["config/type"].to_s.casecmp?(Archsight::Pdf::Theme::TYPE)
89
+
90
+ serve_pdf_preview(config)
91
+ end
92
+
93
+ # GET /api/v1/kinds/:kind/instances/:name/pdf - The resource as PDF document (Typst), see Archsight::Pdf
94
+ get "/api/v1/kinds/:kind/instances/:name/pdf" do
95
+ klass = Archsight::Resources[params[:kind]]
96
+ json_error("Kind '#{params[:kind]}' not found", status: 404, error_type: "NotFound") unless klass
97
+
98
+ instance = db.instance_by_kind(params[:kind], params[:name]) || db.instance_by_alias(params[:kind], params[:name])
99
+ json_error("Instance '#{params[:name]}' not found", status: 404, error_type: "NotFound") unless instance
100
+
101
+ serve_pdf(instance)
102
+ end
103
+
82
104
  # GET /api/v1/pages - Page tree built from PageMenu resources, page tags and filters and the home page
83
105
  # (shown at `/`; it is not in the tree unless a menu contains it)
84
106
  get "/api/v1/pages" do
@@ -95,6 +117,14 @@ module Archsight::Web::API::Routes
95
117
  json_response(build_page_response(page))
96
118
  end
97
119
 
120
+ # GET /api/v1/pages/:name/pdf - The page as PDF document (Typst), see Archsight::Pdf
121
+ get "/api/v1/pages/:name/pdf" do
122
+ page = db.instance_by_kind("Page", params[:name])
123
+ json_error("Page '#{params[:name]}' not found", status: 404, error_type: "NotFound") unless page
124
+
125
+ serve_pdf(page)
126
+ end
127
+
98
128
  # GET /api/v1/search - Search with query language
99
129
  get "/api/v1/search" do
100
130
  query = params[:q]
@@ -179,9 +209,10 @@ module Archsight::Web::API::Routes
179
209
  resolver = Archsight::Helpers::ResourceResolver.new(db)
180
210
  headers "Content-Security-Policy" => "default-src 'none'; style-src 'unsafe-inline'; sandbox"
181
211
  cache_control :public, :must_revalidate, max_age: 0
182
- etag Archsight::Helpers::DiagramBlocks.fingerprint(source, resolver: resolver)
212
+ includes = Archsight::Helpers::DiagramIncludes.for(db)
213
+ etag Archsight::Helpers::DiagramBlocks.fingerprint(source, resolver: resolver, includes: includes)
183
214
  content_type "image/svg+xml"
184
- Archsight::Helpers::DiagramBlocks.standalone_svg(source, resolver: resolver)
215
+ Archsight::Helpers::DiagramBlocks.standalone_svg(source, resolver: resolver, includes: includes)
185
216
  rescue Archsight::Diagram::Error => e
186
217
  json_error("Diagram does not render: #{e.message}", status: 422, error_type: "DiagramError")
187
218
  end
@@ -201,7 +232,7 @@ module Archsight::Web::API::Routes
201
232
  json_error("A JSON body with a 'source' string is required", status: 400, error_type: "BadRequest") unless source.is_a?(String)
202
233
 
203
234
  resolver = Archsight::Helpers::ResourceResolver.new(db)
204
- json_response(Archsight::Helpers::DiagramBlocks.preview(source, resolver: resolver))
235
+ json_response(Archsight::Helpers::DiagramBlocks.preview(source, resolver: resolver, includes: Archsight::Helpers::DiagramIncludes.for(db)))
205
236
  end
206
237
 
207
238
  # POST /api/v1/kinds/Analysis/instances/:name/execute - Execute an analysis
@@ -161,7 +161,8 @@ class Archsight::Web::Application < Sinatra::Base
161
161
  html = Archsight::Markdown.document(data).to_html
162
162
  html = Archsight::Helpers::AssetImages.rewrite(html, base_dir: base, resources_dir: Archsight.resources_dir) if base
163
163
  # ```asd blocks become placeholders until the text passes below are done (see DiagramBlocks)
164
- html, diagrams = Archsight::Helpers::DiagramBlocks.extract(html, resolver: Archsight::Helpers::ResourceResolver.new(db))
164
+ html, diagrams = Archsight::Helpers::DiagramBlocks.extract(html, resolver: Archsight::Helpers::ResourceResolver.new(db),
165
+ includes: Archsight::Helpers::DiagramIncludes.for(db))
165
166
  # so are ```view and ```requirements blocks (see ViewBlocks, RequirementsBlocks)
166
167
  html, views = Archsight::Helpers::ViewBlocks.extract(html)
167
168
  html, requirements = Archsight::Helpers::RequirementsBlocks.extract(html)
@@ -0,0 +1 @@
1
+ import{B as e,L as t,R as n,X as r,it as i,p as a,t as o,tt as s}from"./index-BF4zUlWZ.js";import{t as c}from"./WikiPage-Bi7dzY2h.js";var l={key:1},u={__name:`DocPage`,props:{filename:String},setup(u){let d=u,f=i(null);function p(e,t){let n=new DOMParser().parseFromString(e,`text/html`).body,r=n.querySelector(`:scope > article`)||n,i=r.querySelector(`h1`),a=i?.textContent.trim()||t.split(`/`).pop().replace(/[-_]/g,` `);i?.remove();let o=[...r.querySelectorAll(`h2[id], h3[id], h4[id]`)].map(e=>({level:Number(e.tagName[1]),id:e.id,text:e.textContent.trim()})),s=[{title:`Documentation`}];return t.startsWith(`resources/`)&&s.push({title:`Resource types`}),{title:a,html:r.innerHTML,toc:o.length>=2?o:[],breadcrumb:s}}async function m(){let e=await a(d.filename);f.value=e?p(e,d.filename):{title:`Not found`,html:`<p>Documentation not found.</p>`,toc:[],breadcrumb:[{title:`Documentation`}]},o(f.value.title)}return s(()=>d.filename,m,{immediate:!0}),(i,a)=>f.value?(r(),n(c,{key:0,title:f.value.title,html:f.value.html,breadcrumb:f.value.breadcrumb,toc:f.value.toc},null,8,[`title`,`html`,`breadcrumb`,`toc`])):(r(),e(`article`,l,[...a[0]||=[t(`p`,null,`Loading...`,-1)]]))}};export{u as default};