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.
- checksums.yaml +4 -4
- data/Dockerfile +4 -0
- data/docs/configuration.md +5 -0
- data/docs/diagram.md +69 -0
- data/docs/index.md.erb +1 -0
- data/docs/modeling.md +3 -1
- data/docs/pdf-export.md +118 -0
- data/lib/archsight/analysis/typst_renderer.rb +25 -78
- data/lib/archsight/cli.rb +39 -1
- data/lib/archsight/database.rb +4 -3
- data/lib/archsight/diagram/cli.rb +11 -1
- data/lib/archsight/diagram/errors.rb +1 -0
- data/lib/archsight/diagram/graph/attributes.rb +3 -1
- data/lib/archsight/diagram/graph/node.rb +18 -1
- data/lib/archsight/diagram/includes.rb +168 -0
- data/lib/archsight/diagram/layout/expander.rb +75 -3
- data/lib/archsight/diagram/layout/rank_arranger.rb +3 -1
- data/lib/archsight/diagram/parser/ast.rb +5 -0
- data/lib/archsight/diagram/parser/table_layout.rb +2 -0
- data/lib/archsight/diagram/parser.rb +58 -16
- data/lib/archsight/diagram.rb +20 -4
- data/lib/archsight/export/confluence/storage.rb +6 -1
- data/lib/archsight/helpers/diagram_blocks.rb +34 -19
- data/lib/archsight/helpers/diagram_includes.rb +67 -0
- data/lib/archsight/helpers.rb +1 -0
- data/lib/archsight/linter.rb +10 -2
- data/lib/archsight/markdown.rb +17 -1
- data/lib/archsight/pdf/cache.rb +41 -0
- data/lib/archsight/pdf/compiler.rb +69 -0
- data/lib/archsight/pdf/dependency_graph.rb +127 -0
- data/lib/archsight/pdf/document.rb +227 -0
- data/lib/archsight/pdf/embeds.rb +134 -0
- data/lib/archsight/pdf/images.rb +94 -0
- data/lib/archsight/pdf/markdown.rb +217 -0
- data/lib/archsight/pdf/remote_image.rb +122 -0
- data/lib/archsight/pdf/resource_sheet.rb +129 -0
- data/lib/archsight/pdf/sample_document.rb +67 -0
- data/lib/archsight/pdf/tables.rb +72 -0
- data/lib/archsight/pdf/theme.rb +125 -0
- data/lib/archsight/pdf/typst/archsight.typ +152 -0
- data/lib/archsight/pdf/typst.rb +45 -0
- data/lib/archsight/pdf.rb +30 -0
- data/lib/archsight/references.rb +4 -3
- data/lib/archsight/resources/application_component.rb +10 -0
- data/lib/archsight/resources/application_service.rb +9 -0
- data/lib/archsight/resources/config.rb +147 -0
- data/lib/archsight/resources/technology_service.rb +9 -0
- data/lib/archsight/version.rb +1 -1
- data/lib/archsight/web/api/json_helpers.rb +3 -2
- data/lib/archsight/web/api/openapi/spec.yaml +239 -2
- data/lib/archsight/web/api/page_helpers.rb +1 -1
- data/lib/archsight/web/api/pdf_helpers.rb +46 -0
- data/lib/archsight/web/api/routes.rb +34 -3
- data/lib/archsight/web/application.rb +2 -1
- data/lib/archsight/web/public/vue/DocPage-D3QtnrmO.js +1 -0
- data/lib/archsight/web/public/vue/EditorPage-lZyG6dVt.js +35 -0
- data/lib/archsight/web/public/vue/ErrorPage-BhdYGZJk.js +2 -0
- data/lib/archsight/web/public/vue/GraphView-CoqAjFdR.js +1 -0
- data/lib/archsight/web/public/vue/HomePage-B7SaXVeb.js +2 -0
- data/lib/archsight/web/public/vue/{InstanceRouter-Ca_C8izS.css → InstanceRouter-BCKbW-Wp.css} +1 -1
- data/lib/archsight/web/public/vue/InstanceRouter-DJJn9fhP.js +1 -0
- data/lib/archsight/web/public/vue/KindList-UqMajqbf.js +1 -0
- data/lib/archsight/web/public/vue/PageView-CirIXjW6.js +1 -0
- data/lib/archsight/web/public/vue/QueryError-BIJH6gQC.js +1 -0
- data/lib/archsight/web/public/vue/ResourceList-761vG1Jw.js +2 -0
- data/lib/archsight/web/public/vue/SearchResults-BPHa6yhK.js +1 -0
- data/lib/archsight/web/public/vue/WikiPage-Bi7dzY2h.js +3 -0
- data/lib/archsight/web/public/vue/{WikiPage-TpmM1VAF.css → WikiPage-CuUi4C8v.css} +1 -1
- data/lib/archsight/web/public/vue/{index-yiZbbvAs.js → index-BF4zUlWZ.js} +2 -2
- data/lib/archsight/web/public/vue/index-BO59Fbpf.css +1 -0
- data/lib/archsight/web/public/vue.html +2 -2
- metadata +37 -17
- data/lib/archsight/web/public/vue/DocPage-C8Ysway-.js +0 -1
- data/lib/archsight/web/public/vue/EditorPage-pr2MR6_b.js +0 -35
- data/lib/archsight/web/public/vue/ErrorPage-FAkpW-hn.js +0 -2
- data/lib/archsight/web/public/vue/GraphView-liQJzFyo.js +0 -1
- data/lib/archsight/web/public/vue/HomePage-DYpzSXG8.js +0 -2
- data/lib/archsight/web/public/vue/InstanceRouter-BdkBTmGO.js +0 -1
- data/lib/archsight/web/public/vue/KindList-CrgI4qDb.js +0 -1
- data/lib/archsight/web/public/vue/PageView-BIxT_t49.js +0 -1
- data/lib/archsight/web/public/vue/QueryError-ChdyV8B1.js +0 -1
- data/lib/archsight/web/public/vue/ResourceList-DdtaHY4k.js +0 -2
- data/lib/archsight/web/public/vue/SearchResults-BYRAcK_o.js +0 -1
- data/lib/archsight/web/public/vue/WikiPage-CgvRN6R2.js +0 -3
- 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
|
data/lib/archsight/version.rb
CHANGED
|
@@ -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:
|
|
1586
|
-
|
|
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.
|
|
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
|
-
|
|
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};
|