serverest 3.2.2 → 3.3.0-beta.2

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.
@@ -2,6 +2,8 @@
2
2
 
3
3
  const { validate } = require('express-validation')
4
4
 
5
+ const { etapa } = require('../utils/etapas')
6
+
5
7
  module.exports = schema => {
6
- return validate(schema, { keyByField: false }, { abortEarly: false })
8
+ return etapa('schema', validate(schema, { keyByField: false }, { abortEarly: false }))
7
9
  }
@@ -7,7 +7,7 @@ const STYLES_DIR = join(__dirname, 'styles')
7
7
  const SCRIPTS_DIR = join(__dirname, 'scripts')
8
8
  const TRANSLATIONS_DIR = join(__dirname, 'translations')
9
9
 
10
- const STYLE_FILES = ['topbar.css', 'language-switcher.css', 'release-toast.css']
10
+ const STYLE_FILES = ['topbar.css', 'language-switcher.css', 'release-toast.css', 'flow-panel.css']
11
11
 
12
12
  function loadCustomCss () {
13
13
  return STYLE_FILES
@@ -40,6 +40,14 @@ function buildUiLabelTranslations () {
40
40
  return entries.join(',\n')
41
41
  }
42
42
 
43
+ // O painel sempre renderiza pt-BR e a tradução da página troca o texto, como no resto da UI.
44
+ // ui.response entra porque o painel reaproveita o rótulo "Resposta" já existente.
45
+ function buildFlowPanelTexts () {
46
+ const ptBr = require(join(TRANSLATIONS_DIR, 'pt-BR.js'))
47
+ const keys = Object.keys(ptBr).filter(k => k.startsWith('ui.flow.') || k === 'ui.response')
48
+ return JSON.stringify(keys.reduce((texts, key) => ({ ...texts, [key]: ptBr[key] }), {}))
49
+ }
50
+
43
51
  const customCss = loadCustomCss()
44
52
  const customJsTemplate = loadCustomJsTemplate()
45
53
 
@@ -48,7 +56,8 @@ function buildCustomJsStr (releaseInfo, currentVersion, forceBanner) {
48
56
  __RELEASE_INFO__: JSON.stringify(releaseInfo),
49
57
  __CURRENT_VERSION__: JSON.stringify(currentVersion),
50
58
  __FORCE_BANNER__: JSON.stringify(forceBanner),
51
- __UI_LABEL_TRANSLATIONS__: buildUiLabelTranslations()
59
+ __UI_LABEL_TRANSLATIONS__: buildUiLabelTranslations(),
60
+ __FLOW_PANEL_TEXTS__: buildFlowPanelTexts()
52
61
  }
53
62
  return Object.keys(replacements).reduce(
54
63
  (output, key) => output.split(key).join(replacements[key]),
@@ -0,0 +1,374 @@
1
+ /* Painel "Fluxo da requisição": desenha o header X-ServeRest-Stages abaixo da resposta do "Try it out" */
2
+ // O ponto e vírgula inicial é obrigatório: os scripts desta pasta são concatenados e, sem ele, o
3
+ // navegador interpreta este bloco como uma chamada do resultado do script anterior.
4
+ ;(function () {
5
+ const texts = __FLOW_PANEL_TEXTS__
6
+ const stagesHeader = 'x-serverest-stages'
7
+ const slowMotionKey = 'serverestFlowSlowMotion'
8
+
9
+ function knownKey (key) {
10
+ return Object.prototype.hasOwnProperty.call(texts, key) ? key : null
11
+ }
12
+
13
+ function text (key) {
14
+ return knownKey(key) ? texts[key] : ''
15
+ }
16
+
17
+ function parseEntry (piece) {
18
+ const [name, ...params] = piece.split(';').map(part => part.trim())
19
+ const entry = {}
20
+ params.filter(Boolean).forEach(param => {
21
+ const separator = param.indexOf('=')
22
+ const key = separator === -1 ? param : param.slice(0, separator)
23
+ entry[key] = separator === -1 ? '' : param.slice(separator + 1)
24
+ })
25
+ entry.name = name
26
+ if (entry.dur !== undefined) {
27
+ const dur = Number(entry.dur)
28
+ entry.dur = entry.dur === '' || Number.isNaN(dur) ? undefined : dur
29
+ }
30
+ return entry
31
+ }
32
+
33
+ function parseStages (headerValue) {
34
+ if (typeof headerValue !== 'string') return []
35
+ return headerValue.split(',')
36
+ .map(piece => piece.trim())
37
+ .filter(Boolean)
38
+ .map(parseEntry)
39
+ .filter(entry => entry.name)
40
+ }
41
+
42
+ function extraParams (entry) {
43
+ return Object.keys(entry)
44
+ .filter(key => !['name', 'st', 'dur'].includes(key))
45
+ .reduce((params, key) => Object.assign(params, { [key]: entry[key] }), {})
46
+ }
47
+
48
+ function buildStage (entry) {
49
+ return {
50
+ name: entry.name,
51
+ st: entry.st,
52
+ dur: entry.dur,
53
+ params: extraParams(entry),
54
+ labelKey: knownKey('ui.flow.stage_label.' + entry.name),
55
+ descriptionKey: knownKey('ui.flow.stage_description.' + entry.name),
56
+ statusKey: knownKey('ui.flow.status.' + entry.st),
57
+ width: 0,
58
+ rules: []
59
+ }
60
+ }
61
+
62
+ function buildRule (entry, code) {
63
+ return {
64
+ name: entry.name,
65
+ code,
66
+ st: entry.st,
67
+ item: entry.item,
68
+ params: extraParams(entry),
69
+ labelKey: knownKey('ui.flow.rule_label.' + code),
70
+ statusKey: knownKey('ui.flow.status.' + entry.st)
71
+ }
72
+ }
73
+
74
+ function failureKeys (stage, rule, status) {
75
+ // 408 marca como failed a etapa em execução no timeout, e 500 ou mais é erro inesperado: a etapa não explica a falha.
76
+ if (status === 408) return { whyKey: 'ui.flow.why.status.timeout', howKey: 'ui.flow.how.status.timeout' }
77
+ if (status >= 500) return { whyKey: 'ui.flow.why.status.unexpected_error', howKey: 'ui.flow.how.status.unexpected_error' }
78
+ if (rule) {
79
+ const why = knownKey('ui.flow.why.rule.' + rule.code)
80
+ const how = knownKey('ui.flow.how.rule.' + rule.code)
81
+ if (why && how) return { whyKey: why, howKey: how }
82
+ }
83
+ if (stage.name === 'auth' && stage.params.reason) {
84
+ const why = knownKey('ui.flow.why.auth.' + stage.params.reason)
85
+ const how = knownKey('ui.flow.how.auth.' + stage.params.reason)
86
+ if (why && how) return { whyKey: why, howKey: how }
87
+ }
88
+ return {
89
+ whyKey: knownKey('ui.flow.why.stage.' + stage.name),
90
+ howKey: knownKey('ui.flow.how.stage.' + stage.name)
91
+ }
92
+ }
93
+
94
+ function buildFlowModel (entries, statusCode) {
95
+ if (!Array.isArray(entries) || entries.length === 0) return null
96
+ const stages = []
97
+ entries.forEach(entry => {
98
+ const separator = entry.name.indexOf('.')
99
+ const owner = separator > 0 ? entry.name.slice(0, separator) : null
100
+ const parent = owner ? stages.filter(stage => stage.name === owner).pop() : null
101
+ if (parent) {
102
+ parent.rules.push(buildRule(entry, entry.name.slice(separator + 1)))
103
+ } else {
104
+ stages.push(buildStage(entry))
105
+ }
106
+ })
107
+ const totalDur = stages.reduce((total, stage) => total + (stage.dur || 0), 0)
108
+ stages.forEach(stage => {
109
+ stage.width = totalDur > 0 ? (stage.dur || 0) / totalDur : 0
110
+ })
111
+ const status = Number(statusCode)
112
+ const failedStage = stages.find(stage => stage.st === 'failed') || null
113
+ const failedRule = failedStage ? failedStage.rules.find(rule => rule.st === 'failed') || null : null
114
+ let summary = 'completed'
115
+ let keys = { whyKey: null, howKey: null }
116
+ if (failedStage) {
117
+ summary = 'stopped'
118
+ keys = failureKeys(failedStage, failedRule, status)
119
+ } else if (status >= 400) {
120
+ summary = 'outside'
121
+ keys = { whyKey: 'ui.flow.why.outside', howKey: 'ui.flow.how.outside' }
122
+ }
123
+ return {
124
+ statusCode: Number.isNaN(status) ? null : status,
125
+ summary,
126
+ failedStage: failedStage ? failedStage.name : null,
127
+ failedRule: failedRule ? failedRule.code : null,
128
+ whyKey: keys.whyKey,
129
+ howKey: keys.howKey,
130
+ stages
131
+ }
132
+ }
133
+
134
+ function stepDelay (stage) {
135
+ if (stage.dur === undefined) return 420
136
+ return Math.min(1700, Math.max(420, stage.dur * 480))
137
+ }
138
+
139
+ function prefersReducedMotion () {
140
+ try {
141
+ return !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches)
142
+ } catch (e) {
143
+ return false
144
+ }
145
+ }
146
+
147
+ function readSlowMotion () {
148
+ try {
149
+ return window.localStorage.getItem(slowMotionKey) !== 'false'
150
+ } catch (e) {
151
+ return true
152
+ }
153
+ }
154
+
155
+ function saveSlowMotion (enabled) {
156
+ try {
157
+ window.localStorage.setItem(slowMotionKey, enabled ? 'true' : 'false')
158
+ } catch (e) {}
159
+ }
160
+
161
+ function readResponse (response) {
162
+ try {
163
+ const headers = response && response.get('headers')
164
+ const map = headers && headers.toJS ? headers.toJS() : {}
165
+ const name = Object.keys(map).find(key => key.toLowerCase() === stagesHeader)
166
+ if (!name || map[name] === undefined || map[name] === null) return null
167
+ return { header: String(map[name]), status: response.get('status') }
168
+ } catch (e) {
169
+ return null
170
+ }
171
+ }
172
+
173
+ function createComponents (React) {
174
+ const h = React.createElement
175
+
176
+ class FlowBoundary extends React.Component {
177
+ constructor (props) {
178
+ super(props)
179
+ this.state = { failed: false }
180
+ }
181
+
182
+ static getDerivedStateFromError () {
183
+ return { failed: true }
184
+ }
185
+
186
+ componentDidCatch () {}
187
+
188
+ render () {
189
+ return this.state.failed ? null : this.props.children
190
+ }
191
+ }
192
+
193
+ // Todo texto traduzível fica sozinho no seu nó: a tradução da página compara o nó inteiro.
194
+ function label (key, className) {
195
+ return h('span', { key, className }, text(key))
196
+ }
197
+
198
+ // Código desconhecido vai em <code>, que a tradução da página nunca altera.
199
+ function codeOrLabel (key, raw, className) {
200
+ return key ? label(key, className) : h('code', { key: 'raw-' + raw, className }, String(raw === undefined ? '' : raw))
201
+ }
202
+
203
+ function statusClass (item) {
204
+ return 'serverest-flow-st-' + (item.statusKey ? item.st : 'unknown')
205
+ }
206
+
207
+ function textBlock (titleKey, bodyKey, className) {
208
+ return h('div', { key: titleKey, className: 'serverest-flow-block ' + className },
209
+ h('strong', { className: 'serverest-flow-block-title' }, text(titleKey)),
210
+ h('p', { className: 'serverest-flow-block-text' }, text(bodyKey))
211
+ )
212
+ }
213
+
214
+ function RuleRow (props) {
215
+ const rule = props.rule
216
+ return h('li', { className: 'serverest-flow-rule ' + statusClass(rule) },
217
+ codeOrLabel(rule.statusKey, rule.st, 'serverest-flow-status'),
218
+ codeOrLabel(rule.labelKey, rule.code, 'serverest-flow-name'),
219
+ rule.item !== undefined
220
+ ? h('span', { className: 'serverest-flow-item' },
221
+ label('ui.flow.item_position', 'serverest-flow-item-label'),
222
+ h('span', { className: 'serverest-flow-item-value' }, String(rule.item)))
223
+ : null
224
+ )
225
+ }
226
+
227
+ function StageRow (props) {
228
+ const { stage, model, expanded, onToggle } = props
229
+ const isFailed = model.failedStage === stage.name && stage.st === 'failed'
230
+ const expandable = !!stage.descriptionKey || (isFailed && !!model.whyKey)
231
+ const content = [
232
+ codeOrLabel(stage.statusKey, stage.st, 'serverest-flow-status'),
233
+ codeOrLabel(stage.labelKey, stage.name, 'serverest-flow-name'),
234
+ stage.dur !== undefined ? h('span', { className: 'serverest-flow-dur' }, stage.dur.toFixed(2) + ' ms') : null
235
+ ]
236
+ const head = expandable
237
+ ? h('button', { type: 'button', className: 'serverest-flow-row', 'aria-expanded': expanded ? 'true' : 'false', onClick: onToggle }, ...content)
238
+ : h('div', { className: 'serverest-flow-row' }, ...content)
239
+ const details = expandable && expanded
240
+ ? h('div', { className: 'serverest-flow-details' },
241
+ stage.descriptionKey ? textBlock('ui.flow.stage_description_title', stage.descriptionKey, 'serverest-flow-block-description') : null,
242
+ isFailed && model.whyKey ? textBlock('ui.flow.why_title', model.whyKey, 'serverest-flow-block-why') : null,
243
+ isFailed && model.howKey ? textBlock('ui.flow.how_title', model.howKey, 'serverest-flow-block-how') : null)
244
+ : null
245
+ const rules = stage.rules.length
246
+ ? h('ol', { className: 'serverest-flow-rules' }, stage.rules.map(rule => h(RuleRow, { key: rule.name, rule })))
247
+ : null
248
+ return h('li', { className: 'serverest-flow-stage ' + statusClass(stage) + (isFailed ? ' serverest-flow-stage-failed' : '') }, head, details, rules)
249
+ }
250
+
251
+ function Summary (props) {
252
+ const model = props.model
253
+ if (model.summary === 'stopped') {
254
+ const stage = model.stages.find(item => item.name === model.failedStage)
255
+ return h('p', { key: 'stopped-' + stage.name, className: 'serverest-flow-summary' },
256
+ label('ui.flow.stopped_at', 'serverest-flow-summary-label'),
257
+ codeOrLabel(stage.labelKey, stage.name, 'serverest-flow-summary-stage'))
258
+ }
259
+ const key = model.summary === 'outside' ? 'ui.flow.outside' : 'ui.flow.completed'
260
+ return h('p', { key, className: 'serverest-flow-summary' }, label(key, 'serverest-flow-summary-label'))
261
+ }
262
+
263
+ function FlowPanel (props) {
264
+ const { model, animate, showControls, slowMotion, onToggleSlowMotion, onReplay } = props
265
+ const executed = model.stages.filter(stage => stage.st !== 'not_run')
266
+ const [revealed, setRevealed] = React.useState(animate ? 0 : executed.length)
267
+ const [expanded, setExpanded] = React.useState(model.failedStage ? { [model.failedStage]: true } : {})
268
+ const done = revealed >= executed.length
269
+
270
+ React.useEffect(() => {
271
+ if (done) return undefined
272
+ if (!animate) {
273
+ setRevealed(executed.length)
274
+ return undefined
275
+ }
276
+ const timer = setTimeout(() => setRevealed(count => count + 1), stepDelay(executed[revealed]))
277
+ return () => clearTimeout(timer)
278
+ }, [revealed, animate, done])
279
+
280
+ const visible = model.stages.filter(stage => {
281
+ if (stage.st === 'not_run') return done
282
+ return executed.indexOf(stage) <= revealed
283
+ })
284
+ const active = done ? null : executed[revealed]
285
+ const listed = visible.filter(stage => stage !== active)
286
+
287
+ const segments = visible.map(stage => h('span', {
288
+ key: stage.name,
289
+ className: 'serverest-flow-segment ' + statusClass(stage) + (stage === active ? ' serverest-flow-active' : ''),
290
+ style: { flexGrow: stage.width }
291
+ }, codeOrLabel(stage.labelKey, stage.name, 'serverest-flow-segment-label')))
292
+ if (done) {
293
+ segments.push(h('span', { key: 'response', className: 'serverest-flow-segment serverest-flow-segment-response' },
294
+ label('ui.response', 'serverest-flow-segment-label'),
295
+ model.statusCode !== null ? h('span', { className: 'serverest-flow-segment-code' }, String(model.statusCode)) : null))
296
+ }
297
+
298
+ const controls = showControls
299
+ ? h('div', { className: 'serverest-flow-controls' },
300
+ h('label', { className: 'serverest-flow-slow-motion' },
301
+ h('input', { type: 'checkbox', checked: slowMotion, onChange: onToggleSlowMotion }),
302
+ label('ui.flow.slow_motion', 'serverest-flow-slow-motion-label')),
303
+ h('button', { type: 'button', className: 'serverest-flow-replay', onClick: onReplay }, text('ui.flow.replay')))
304
+ : null
305
+
306
+ return h('section', { className: 'serverest-flow' },
307
+ h('div', { className: 'serverest-flow-header' },
308
+ h('h4', { className: 'serverest-flow-title' }, text('ui.flow.title')),
309
+ done ? h(Summary, { key: 'summary-' + model.summary, model }) : null,
310
+ controls),
311
+ h('div', { className: 'serverest-flow-bar' }, segments),
312
+ done && model.summary === 'outside'
313
+ ? h('p', { className: 'serverest-flow-outside' },
314
+ label(model.whyKey, 'serverest-flow-outside-why'),
315
+ label(model.howKey, 'serverest-flow-outside-how'))
316
+ : null,
317
+ h('ol', { className: 'serverest-flow-stages' }, listed.map(stage => h(StageRow, {
318
+ key: stage.name,
319
+ stage,
320
+ model,
321
+ expanded: !!expanded[stage.name],
322
+ onToggle: () => setExpanded(current => Object.assign({}, current, { [stage.name]: !current[stage.name] }))
323
+ })))
324
+ )
325
+ }
326
+
327
+ function FlowRoot (props) {
328
+ const [run, setRun] = React.useState(0)
329
+ const [slowMotion, setSlowMotion] = React.useState(readSlowMotion)
330
+ const { header, status } = props
331
+ const model = buildFlowModel(parseStages(header), status)
332
+ if (!model) return null
333
+ const reduced = prefersReducedMotion()
334
+ return h(FlowPanel, {
335
+ // Remontar a cada resposta ou repetição evita que o React edite um nó já traduzido.
336
+ key: header + '|' + status + '|' + run,
337
+ model,
338
+ animate: slowMotion && !reduced,
339
+ showControls: !reduced,
340
+ slowMotion,
341
+ onToggleSlowMotion: event => {
342
+ const enabled = !!event.target.checked
343
+ saveSlowMotion(enabled)
344
+ setSlowMotion(enabled)
345
+ },
346
+ onReplay: () => setRun(count => count + 1)
347
+ })
348
+ }
349
+
350
+ return { FlowBoundary, FlowRoot }
351
+ }
352
+
353
+ function serverestFlowPanelPlugin (system) {
354
+ const React = system && system.React
355
+ if (!React || !React.Component || !React.useState) return {}
356
+ const { FlowBoundary, FlowRoot } = createComponents(React)
357
+ const h = React.createElement
358
+ return {
359
+ wrapComponents: {
360
+ liveResponse: Original => function ServerestLiveResponse (props) {
361
+ const data = readResponse(props.response)
362
+ const panel = data
363
+ ? h(FlowBoundary, { key: data.header + '|' + data.status }, h(FlowRoot, { header: data.header, status: data.status }))
364
+ : null
365
+ return h(React.Fragment, null, h(Original, props), panel)
366
+ }
367
+ }
368
+ }
369
+ }
370
+
371
+ serverestFlowPanelPlugin.parseStages = parseStages
372
+ serverestFlowPanelPlugin.buildFlowModel = buildFlowModel
373
+ window.serverestFlowPanelPlugin = serverestFlowPanelPlugin
374
+ })()
@@ -0,0 +1,231 @@
1
+ /* Painel "Fluxo da requisição" abaixo da resposta do "Try it out" */
2
+ .swagger-ui .serverest-flow {
3
+ margin: 16px 0 8px;
4
+ padding: 14px 16px;
5
+ border: 1px solid #d8dde7;
6
+ border-radius: 8px;
7
+ background: #ffffff;
8
+ color: #1f2937;
9
+ font-family: sans-serif;
10
+ font-size: 13px;
11
+ }
12
+
13
+ .swagger-ui .serverest-flow-header {
14
+ display: flex;
15
+ flex-wrap: wrap;
16
+ align-items: center;
17
+ gap: 8px 16px;
18
+ margin-bottom: 12px;
19
+ }
20
+
21
+ .swagger-ui .serverest-flow-title {
22
+ margin: 0;
23
+ font-size: 14px;
24
+ font-weight: 700;
25
+ }
26
+
27
+ .swagger-ui .serverest-flow-summary {
28
+ display: inline-flex;
29
+ gap: 6px;
30
+ margin: 0;
31
+ font-weight: 600;
32
+ }
33
+
34
+ .swagger-ui .serverest-flow-controls {
35
+ display: inline-flex;
36
+ align-items: center;
37
+ gap: 12px;
38
+ margin-left: auto;
39
+ }
40
+
41
+ .swagger-ui .serverest-flow-slow-motion {
42
+ display: inline-flex;
43
+ align-items: center;
44
+ gap: 6px;
45
+ cursor: pointer;
46
+ }
47
+
48
+ .swagger-ui .serverest-flow-replay {
49
+ padding: 4px 12px;
50
+ border: 1px solid #9ca3af;
51
+ border-radius: 4px;
52
+ background: #ffffff;
53
+ font-size: 12px;
54
+ cursor: pointer;
55
+ }
56
+
57
+ .swagger-ui .serverest-flow-bar {
58
+ display: flex;
59
+ flex-wrap: wrap;
60
+ gap: 3px;
61
+ min-height: 26px;
62
+ margin-bottom: 12px;
63
+ }
64
+
65
+ .swagger-ui .serverest-flow-segment {
66
+ display: flex;
67
+ flex-basis: 0;
68
+ flex-shrink: 0;
69
+ align-items: center;
70
+ justify-content: center;
71
+ gap: 4px;
72
+ min-width: max-content;
73
+ min-height: 26px;
74
+ padding: 0 8px;
75
+ border-radius: 4px;
76
+ font-size: 11px;
77
+ white-space: nowrap;
78
+ overflow: hidden;
79
+ animation: serverestFlowIn 220ms ease-out;
80
+ }
81
+
82
+ .swagger-ui .serverest-flow-segment-response {
83
+ flex-grow: 0;
84
+ background: #1f2937;
85
+ color: #ffffff;
86
+ }
87
+
88
+ .swagger-ui .serverest-flow-segment.serverest-flow-active {
89
+ animation: serverestFlowPulse 900ms ease-in-out infinite;
90
+ }
91
+
92
+ .swagger-ui .serverest-flow-st-ok.serverest-flow-segment { background: #2f9e6b; color: #ffffff; }
93
+ .swagger-ui .serverest-flow-st-failed.serverest-flow-segment { background: #c53030; color: #ffffff; }
94
+ .swagger-ui .serverest-flow-st-skipped.serverest-flow-segment { background: #6b7280; color: #ffffff; }
95
+ .swagger-ui .serverest-flow-st-not_run.serverest-flow-segment { background: #e5e7eb; color: #4b5563; }
96
+ .swagger-ui .serverest-flow-st-unknown.serverest-flow-segment { background: #d1d5db; color: #1f2937; }
97
+
98
+ .swagger-ui .serverest-flow-outside {
99
+ display: flex;
100
+ flex-wrap: wrap;
101
+ gap: 4px;
102
+ margin: 0 0 12px;
103
+ }
104
+
105
+ .swagger-ui .serverest-flow-stages,
106
+ .swagger-ui .serverest-flow-rules {
107
+ list-style: none;
108
+ margin: 0;
109
+ padding: 0;
110
+ }
111
+
112
+ .swagger-ui .serverest-flow-stage {
113
+ border-top: 1px solid #eef1f5;
114
+ animation: serverestFlowIn 220ms ease-out;
115
+ }
116
+
117
+ .swagger-ui .serverest-flow-row {
118
+ display: flex;
119
+ align-items: center;
120
+ gap: 10px;
121
+ width: 100%;
122
+ padding: 6px 0;
123
+ border: 0;
124
+ background: none;
125
+ color: inherit;
126
+ font: inherit;
127
+ text-align: left;
128
+ }
129
+
130
+ .swagger-ui button.serverest-flow-row {
131
+ cursor: pointer;
132
+ }
133
+
134
+ .swagger-ui .serverest-flow-status {
135
+ flex-shrink: 0;
136
+ min-width: 11em;
137
+ padding: 1px 8px;
138
+ border-radius: 10px;
139
+ font-size: 11px;
140
+ text-align: center;
141
+ }
142
+
143
+ .swagger-ui .serverest-flow-st-ok > .serverest-flow-row .serverest-flow-status,
144
+ .swagger-ui .serverest-flow-rule.serverest-flow-st-ok .serverest-flow-status { background: #d1fae5; color: #065f46; }
145
+ .swagger-ui .serverest-flow-st-failed > .serverest-flow-row .serverest-flow-status,
146
+ .swagger-ui .serverest-flow-rule.serverest-flow-st-failed .serverest-flow-status { background: #fee2e2; color: #991b1b; }
147
+ .swagger-ui .serverest-flow-st-skipped > .serverest-flow-row .serverest-flow-status,
148
+ .swagger-ui .serverest-flow-rule.serverest-flow-st-skipped .serverest-flow-status { background: #e5e7eb; color: #374151; }
149
+ .swagger-ui .serverest-flow-st-not_run > .serverest-flow-row .serverest-flow-status,
150
+ .swagger-ui .serverest-flow-rule.serverest-flow-st-not_run .serverest-flow-status { background: #f3f4f6; color: #6b7280; }
151
+
152
+ .swagger-ui .serverest-flow-name {
153
+ font-weight: 600;
154
+ }
155
+
156
+ .swagger-ui .serverest-flow-dur {
157
+ margin-left: auto;
158
+ color: #6b7280;
159
+ font-variant-numeric: tabular-nums;
160
+ }
161
+
162
+ .swagger-ui .serverest-flow-stage-failed > .serverest-flow-row .serverest-flow-name {
163
+ color: #991b1b;
164
+ }
165
+
166
+ .swagger-ui .serverest-flow-details {
167
+ display: flex;
168
+ flex-direction: column;
169
+ gap: 8px;
170
+ padding: 0 0 10px 22px;
171
+ }
172
+
173
+ .swagger-ui .serverest-flow-block-title {
174
+ display: block;
175
+ margin-bottom: 2px;
176
+ font-size: 12px;
177
+ }
178
+
179
+ .swagger-ui .serverest-flow-block-text {
180
+ margin: 0;
181
+ }
182
+
183
+ .swagger-ui .serverest-flow-block-why,
184
+ .swagger-ui .serverest-flow-block-how {
185
+ padding: 6px 10px;
186
+ border-left: 3px solid #c53030;
187
+ background: #fef2f2;
188
+ }
189
+
190
+ .swagger-ui .serverest-flow-block-how {
191
+ border-left-color: #2f9e6b;
192
+ background: #f0fdf4;
193
+ }
194
+
195
+ .swagger-ui .serverest-flow-rule {
196
+ display: flex;
197
+ flex-wrap: wrap;
198
+ align-items: center;
199
+ gap: 10px;
200
+ padding: 4px 0 4px 22px;
201
+ color: #4b5563;
202
+ }
203
+
204
+ .swagger-ui .serverest-flow-item {
205
+ display: inline-flex;
206
+ gap: 4px;
207
+ margin-left: auto;
208
+ }
209
+
210
+ @keyframes serverestFlowIn {
211
+ 0% { opacity: 0; }
212
+ 100% { opacity: 1; }
213
+ }
214
+
215
+ @keyframes serverestFlowPulse {
216
+ 0% { opacity: 0.45; }
217
+ 50% { opacity: 1; }
218
+ 100% { opacity: 0.45; }
219
+ }
220
+
221
+ @media (prefers-reduced-motion: reduce) {
222
+ .swagger-ui .serverest-flow-segment,
223
+ .swagger-ui .serverest-flow-stage,
224
+ .swagger-ui .serverest-flow-segment.serverest-flow-active {
225
+ animation: none;
226
+ }
227
+
228
+ .swagger-ui .serverest-flow-controls {
229
+ display: none;
230
+ }
231
+ }