@pb33f/cowboy-components 0.1.16 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/assets/rule-documentation.worker-D39NS8Lx.js +1 -0
  2. package/dist/components/editor/editor.d.ts +2 -0
  3. package/dist/components/editor/editor.js +6 -4
  4. package/dist/components/error-banner/error-banner.css.js +1 -1
  5. package/dist/components/manage-ruleset/function-option.css.d.ts +2 -0
  6. package/dist/components/manage-ruleset/function-option.css.js +38 -0
  7. package/dist/components/manage-ruleset/function-option.d.ts +19 -0
  8. package/dist/components/manage-ruleset/function-option.js +117 -0
  9. package/dist/components/manage-ruleset/manage-ruleset.css.d.ts +2 -0
  10. package/dist/components/manage-ruleset/manage-ruleset.css.js +75 -0
  11. package/dist/components/manage-ruleset/manage-ruleset.d.ts +66 -0
  12. package/dist/components/manage-ruleset/manage-ruleset.js +597 -0
  13. package/dist/components/manage-ruleset/rule-action.css.d.ts +2 -0
  14. package/dist/components/manage-ruleset/rule-action.css.js +56 -0
  15. package/dist/components/manage-ruleset/rule-action.d.ts +37 -0
  16. package/dist/components/manage-ruleset/rule-action.js +351 -0
  17. package/dist/components/manage-ruleset/rule-input.d.ts +38 -0
  18. package/dist/components/manage-ruleset/rule-input.js +296 -0
  19. package/dist/components/manage-ruleset/rule.css.d.ts +2 -0
  20. package/dist/components/manage-ruleset/rule.css.js +117 -0
  21. package/dist/components/manage-ruleset/rule.d.ts +31 -0
  22. package/dist/components/manage-ruleset/rule.js +153 -0
  23. package/dist/components/problem-list/details-drawer.d.ts +2 -1
  24. package/dist/components/problem-list/details-drawer.js +7 -0
  25. package/dist/components/problem-list/filter.css.js +2 -3
  26. package/dist/components/problem-list/problem-item.css.js +1 -3
  27. package/dist/components/problem-list/problem-item.js +1 -1
  28. package/dist/components/problem-list/problem-list.css.js +0 -10
  29. package/dist/components/problem-list/problem-list.js +0 -1
  30. package/dist/components/problems-overview/document-statistic.css.js +0 -1
  31. package/dist/components/problems-overview/problem-overview-group.css.js +1 -3
  32. package/dist/components/problems-overview/problem-overview-group.js +2 -2
  33. package/dist/components/problems-overview/problem-statistics.css.js +0 -5
  34. package/dist/components/problems-overview/problems-overview.css.js +0 -4
  35. package/dist/components/the-doctor/the-doctor.css.js +99 -27
  36. package/dist/components/the-doctor/the-doctor.d.ts +66 -8
  37. package/dist/components/the-doctor/the-doctor.js +668 -63
  38. package/dist/components/toast/toast-component.css.d.ts +2 -0
  39. package/dist/components/toast/toast-component.css.js +151 -0
  40. package/dist/components/toast/toast-component.d.ts +19 -0
  41. package/dist/components/toast/toast-component.js +116 -0
  42. package/dist/components/toast/toast-manager.d.ts +13 -0
  43. package/dist/components/toast/toast-manager.js +54 -0
  44. package/dist/cowboy-components.umd.cjs +1375 -372
  45. package/dist/css/button.css.js +46 -0
  46. package/dist/css/dialog.css.d.ts +2 -0
  47. package/dist/css/dialog.css.js +11 -0
  48. package/dist/css/forms.css.d.ts +2 -0
  49. package/dist/css/forms.css.js +123 -0
  50. package/dist/css/modal.css.d.ts +2 -0
  51. package/dist/css/modal.css.js +15 -0
  52. package/dist/css/pb33f-theme.css +1 -0
  53. package/dist/css/radiogroups.css.d.ts +2 -0
  54. package/dist/css/radiogroups.css.js +26 -0
  55. package/dist/css/spinner.css.d.ts +2 -0
  56. package/dist/css/spinner.css.js +42 -0
  57. package/dist/events/doctor.d.ts +61 -3
  58. package/dist/events/doctor.js +14 -1
  59. package/dist/model/errors.d.ts +10 -0
  60. package/dist/model/rule_documentation.d.ts +8 -2
  61. package/dist/model/rule_documentation.js +5 -1
  62. package/dist/model/toast.d.ts +15 -0
  63. package/dist/model/toast.js +9 -0
  64. package/dist/model/vacuum_rule.d.ts +58 -0
  65. package/dist/model/vacuum_rule.js +1 -0
  66. package/dist/services/linting-service.d.ts +1 -1
  67. package/dist/services/linting-service.js +2 -6
  68. package/dist/services/ruleset-service.d.ts +17 -0
  69. package/dist/services/ruleset-service.js +316 -0
  70. package/dist/style.css +1 -1
  71. package/dist/workers/rule-documentation.worker.d.ts +7 -4
  72. package/dist/workers/rule-documentation.worker.js +93 -2
  73. package/package.json +1 -1
  74. package/dist/assets/rule-documentation.worker-BFIxMBU8.js +0 -1
@@ -11,10 +11,11 @@ import '@shoelace-style/shoelace/dist/components/tab/tab.js';
11
11
  import '@shoelace-style/shoelace/dist/components/switch/switch.js';
12
12
  import '@shoelace-style/shoelace/dist/components/dialog/dialog.js';
13
13
  import '@shoelace-style/shoelace/dist/components/alert/alert.js';
14
- import { customElement, property, query } from "lit/decorators.js";
14
+ import '@shoelace-style/shoelace/dist/components/badge/badge.js';
15
+ import { customElement, property, query, state } from "lit/decorators.js";
15
16
  import { html, LitElement } from "lit";
16
17
  import { SpecEditor } from "../editor/editor.js";
17
- import { ActiveView, EditorClicked, EditorUpdated, OpenProblemDrawer, ProblemClicked, ProblemRuleFilterChangedManual, RuleClicked } from "../../events/doctor.js";
18
+ import { ActiveView, AddToast, CustomRulesetEnabled, EditorClicked, EditorUpdated, ExportRuleset, OpenProblemDrawer, ProblemClicked, ProblemRuleFilterChangedManual, RuleViolationClicked, RulesetSaved, RuleClicked, BuiltInRulesetChanged, } from "../../events/doctor.js";
18
19
  import { ProblemDetailsDrawer, ProblemDrawerEventType } from "../problem-list/details-drawer.js";
19
20
  import { CreateBagManager } from "@pb33f/saddlebag";
20
21
  import { LintingService } from "../../services/linting-service.js";
@@ -26,26 +27,44 @@ import { StatusBar } from "./status-bar.js";
26
27
  import { FeedbackComponent } from "./feedback.js";
27
28
  import { FeedbackService } from "../../services/feedback-service.js";
28
29
  import { ActivitySpinner } from "./activity-spinner.js";
30
+ import { ManageRuleset } from "../manage-ruleset/manage-ruleset.js";
29
31
  import RuleDocumentationWorker from "../../workers/rule-documentation.worker.js?worker";
30
32
  import theDoctorCss from "./the-doctor.css.js";
31
33
  import linksCss from "../../css/links.css.js";
34
+ import { RulesetService } from "../../services/ruleset-service.js";
35
+ import { ToastManager } from "../toast/toast-manager.js";
36
+ import { ToastType } from "../../model/toast.js";
37
+ import dialogCss from "../../css/dialog.css.js";
38
+ import buttonCss from "../../css/button.css.js";
39
+ import radioGroupsCss from "../../css/radiogroups.css.js";
40
+ import { MarkerSeverity } from "monaco-editor";
32
41
  export const DoctorDocumentBag = "pb33f-doctor-editor";
33
42
  export const HowToFixBag = "pb33f-doctor-howtofix";
43
+ export const FunctionDocumentationBag = "pb33f-doctor-funcdocs";
34
44
  export const RuleDocumentationBag = "pb33f-doctor-ruledocs";
35
45
  export const DiagnosticBag = "pb33f-doctor-diagnostic";
36
- export const OWASPBag = "pb33f-doctor-owasp";
46
+ export const DefaultRulesetBag = "pb33f-doctor-default-ruleset";
47
+ export const OWASPRulesetBag = "pb33f-doctor-owasp-ruleset";
48
+ export const AllRulesetBag = "pb33f-doctor-all-ruleset";
49
+ export const FunctionsBag = "pb33f-doctor-functions";
50
+ export const FunctionsSchemaBag = "pb33f-doctor-function-schema";
51
+ export const CustomRulesetBag = "pb33f-doctor-custom-ruleset";
52
+ export const RuleConfigurationBag = "pb33f-doctor-rule-configuration";
53
+ export const SessionRulesetMapBag = "pb33f-doctor-session-rulesetmap";
37
54
  export const DefaultDocument = "document";
38
55
  export const DocumentProblems = "problems";
39
56
  export const DoctorEndpoint = "doctor-endpoint";
40
57
  let TheDoctor = class TheDoctor extends LitElement {
41
58
  constructor(doctorEndpoint = "https://doctor.pb33f.io") {
42
59
  super();
43
- this.debounceTime = 300;
60
+ this.debounceTime = 400;
61
+ this.debounceTimeRuleset = 900;
44
62
  this.bounceId = 0;
45
63
  // create a stateful bag manager
46
64
  this.bagManager = CreateBagManager(true);
47
65
  this.bagManager.loadStatefulBags().then(this.loadState.bind(this));
48
66
  this.editor = new SpecEditor();
67
+ this.rulesetEditor = new SpecEditor('ruleset');
49
68
  this.problemList = new ProblemList();
50
69
  this.detailsDrawer = new ProblemDetailsDrawer();
51
70
  this.problemsOverview = new ProblemsOverview();
@@ -55,6 +74,13 @@ let TheDoctor = class TheDoctor extends LitElement {
55
74
  this.unavailable = false;
56
75
  this.feedback = new FeedbackComponent();
57
76
  this.activitySpinner = new ActivitySpinner();
77
+ this.manageRuleset = new ManageRuleset();
78
+ this.toastManager = new ToastManager();
79
+ this.editorMap = new Map();
80
+ this.editorMap.set("spec", this.editor);
81
+ this.editorMap.set("ruleset", this.rulesetEditor);
82
+ this.selectedEditorTab = "spec";
83
+ this.sidebarClosed = false;
58
84
  // extract the doctor endpoint from session storage.
59
85
  const sessionEndpoint = sessionStorage.getItem(DoctorEndpoint);
60
86
  if (sessionEndpoint) {
@@ -74,23 +100,43 @@ let TheDoctor = class TheDoctor extends LitElement {
74
100
  // @ts-ignore
75
101
  this.addEventListener(OpenProblemDrawer, this.ruleDocsClicked);
76
102
  // @ts-ignore
77
- this.addEventListener(RuleClicked, this.ruleGroupClicked);
103
+ this.addEventListener(RuleViolationClicked, this.ruleGroupClicked);
104
+ // @ts-ignore
105
+ this.addEventListener(CustomRulesetEnabled, this.customRulesetEnabled);
106
+ // @ts-ignore
107
+ this.addEventListener(RulesetSaved, this.rulesetSaved);
108
+ // @ts-ignore
109
+ this.addEventListener(AddToast, this.addToastEvent);
110
+ this.addEventListener(ExportRuleset, this.exportRuleset);
111
+ // @ts-ignore
112
+ this.addEventListener(RuleClicked, this.ruleClicked);
113
+ // @ts-ignore
114
+ this.addEventListener(BuiltInRulesetChanged, this.builtInRulesetSelected);
78
115
  // hijack navigation buttons.
79
116
  window.addEventListener('popstate', (e) => {
80
117
  const state = e.state;
81
118
  if (state) {
82
119
  switch (state.view) {
83
120
  case ActiveView.Problems:
84
- this.tabGroup.show(ActiveView.Problems);
121
+ this.controlTabGroup.show(ActiveView.Problems);
85
122
  break;
86
123
  case ActiveView.Overview:
87
- this.tabGroup.show(ActiveView.Overview);
124
+ this.controlTabGroup.show(ActiveView.Overview);
88
125
  break;
89
126
  }
90
127
  }
91
128
  });
92
129
  //history.pushState({view: ActiveView.Overview}, "", ActiveView.Overview);
93
130
  }
131
+ exportRuleset() {
132
+ this.exportRulesetDialog.show();
133
+ }
134
+ addToastEvent(event) {
135
+ this.sendToast(event.detail.toast);
136
+ }
137
+ sendToast(toast) {
138
+ this.toastManager.addToastManually(toast);
139
+ }
94
140
  ruleGroupClicked(event) {
95
141
  this.problemsPanel.focus();
96
142
  const simEvent = new CustomEvent(ProblemRuleFilterChangedManual, {
@@ -103,7 +149,7 @@ let TheDoctor = class TheDoctor extends LitElement {
103
149
  this.problemsPanel.focus();
104
150
  this.problemList.dispatchEvent(simEvent);
105
151
  history.pushState({ rule: event.detail.rule }, "", `/${ActiveView.Problems}?rule=${event.detail.rule}`);
106
- this.tabGroup.show(ActiveView.Problems);
152
+ this.controlTabGroup.show(ActiveView.Problems);
107
153
  }
108
154
  ruleDocsClicked(event) {
109
155
  const ruleId = event.detail.rule;
@@ -115,6 +161,14 @@ let TheDoctor = class TheDoctor extends LitElement {
115
161
  event.detail.body = ruleDoc.body;
116
162
  this.detailsDrawer.open(event.detail);
117
163
  }
164
+ else {
165
+ this.sendToast({
166
+ id: crypto.randomUUID(),
167
+ type: ToastType.INFO,
168
+ title: "Rule documentation unavailable",
169
+ body: `Documentation for '${ruleId}' not available`
170
+ });
171
+ }
118
172
  break;
119
173
  case ProblemDrawerEventType.HOW_TO_FIX:
120
174
  const howToFix = this.howToFixBag?.get(ruleId);
@@ -122,12 +176,64 @@ let TheDoctor = class TheDoctor extends LitElement {
122
176
  event.detail.body = howToFix.howToFix;
123
177
  this.detailsDrawer.open(event.detail);
124
178
  }
179
+ else {
180
+ this.sendToast({
181
+ id: crypto.randomUUID(),
182
+ type: ToastType.INFO,
183
+ title: "How to fix unavailable",
184
+ body: `Information on how to fix '${ruleId}' not available`
185
+ });
186
+ }
187
+ break;
188
+ case ProblemDrawerEventType.FUNCTION_DOCS:
189
+ const funcDocs = this.functionDocsBag?.get(ruleId);
190
+ if (funcDocs) {
191
+ event.detail.body = funcDocs.body;
192
+ this.detailsDrawer.open(event.detail);
193
+ }
194
+ else {
195
+ this.sendToast({
196
+ id: crypto.randomUUID(),
197
+ type: ToastType.INFO,
198
+ title: "Function documentation unavailable",
199
+ body: `Documentation for '${ruleId}' not available`
200
+ });
201
+ }
202
+ }
203
+ }
204
+ }
205
+ rulesetSaved(evt) {
206
+ // lint spec
207
+ if (this.docBag) {
208
+ const doc = this.docBag.get(DefaultDocument);
209
+ const config = this.RuleConfigBag?.get(RuleConfigurationBag);
210
+ if (evt.detail.rules) {
211
+ this.CustomRulesetBag?.set(CustomRulesetBag, evt.detail.rules);
212
+ // get ruleset config and disable everything,
213
+ // then enable the custom rules.
214
+ if (config) {
215
+ // extract config from the event
216
+ const newConfig = evt.detail.config;
217
+ if (newConfig && this.RuleConfigBag) {
218
+ newConfig.returnedRuleset = evt.detail.returnedRules;
219
+ this.RuleConfigBag?.set(RuleConfigurationBag, newConfig);
220
+ }
221
+ }
222
+ RulesetService.getSessionRulesetAsYAML().then((yaml) => {
223
+ if (this.selectedEditorTab == "spec") {
224
+ this.rulesetPulse = true;
225
+ }
226
+ this.rulesetEditor.setValue(yaml, true);
227
+ this.fetchRulesetMap();
228
+ });
229
+ // lint the spec
230
+ this.lintSpec(doc);
125
231
  }
126
232
  }
127
233
  }
128
234
  lintSpec(value) {
129
235
  this.activitySpinner.show();
130
- LintingService.lintFile(value, this.OWASPEnabled).then((result) => {
236
+ LintingService.lintFile(value).then((result) => {
131
237
  if (result && !Array.isArray(result)) {
132
238
  this.editor.setMarkers([result]);
133
239
  this.problemBag?.set(DocumentProblems, [result]);
@@ -144,7 +250,10 @@ let TheDoctor = class TheDoctor extends LitElement {
144
250
  }
145
251
  // update the overview statistics
146
252
  LintingService.fetchStatistics().then((result) => {
147
- this.problemsOverview.statistics = result;
253
+ let oldScore = 0;
254
+ if (this.problemsOverview.statistics) {
255
+ oldScore = this.problemsOverview.statistics.statistics.overallScore;
256
+ }
148
257
  this.diagnosticBag?.set(DiagnosticBag, result);
149
258
  this.activitySpinner.hide();
150
259
  if (result?.remainingCredit <= 10) {
@@ -153,8 +262,29 @@ let TheDoctor = class TheDoctor extends LitElement {
153
262
  console.warn("You are running low on credit, you will need to authenticate soon. " +
154
263
  "" + result.remainingCredit + " credits remaining.");
155
264
  }
265
+ // determine if the score went up or down and toast it!
266
+ this.problemsOverview.statistics = result;
267
+ if (this.problemsOverview.statistics) {
268
+ const newScore = result.statistics.overallScore;
269
+ if (oldScore > newScore) {
270
+ this.sendToast({
271
+ id: crypto.randomUUID(),
272
+ type: ToastType.SCOREDOWN,
273
+ body: "Your score has decreased. It is now " + newScore + "%",
274
+ title: "Score went down by " + (oldScore - newScore) + "%"
275
+ });
276
+ }
277
+ if (oldScore < newScore) {
278
+ this.sendToast({
279
+ id: crypto.randomUUID(),
280
+ type: ToastType.SCOREUP,
281
+ body: "Your score has increased to " + newScore + "%",
282
+ title: "Score went up by " + (newScore - oldScore) + "%"
283
+ });
284
+ }
285
+ }
156
286
  }).catch((e) => {
157
- console.error("statistics service is down");
287
+ console.error("statistics service is down", e);
158
288
  this.platformUnavailable(e);
159
289
  });
160
290
  }).catch((e) => {
@@ -165,6 +295,12 @@ let TheDoctor = class TheDoctor extends LitElement {
165
295
  if (e.instance === 'https://pb33f.io/errors/no-credit-remaining') {
166
296
  this.statusBar.callsRemaining = 0;
167
297
  this.statusBar.visible = true;
298
+ this.sendToast({
299
+ id: crypto.randomUUID(),
300
+ type: ToastType.ERROR,
301
+ body: "Run out of credit, please authenticate for more or wait 24 hours.",
302
+ title: "Credit exhausted!",
303
+ });
168
304
  }
169
305
  }
170
306
  });
@@ -184,20 +320,45 @@ let TheDoctor = class TheDoctor extends LitElement {
184
320
  this.errorBanner.visible = true;
185
321
  this.unavailable = true;
186
322
  this.problemsOverview.unavailable = true;
187
- this.tabGroup.show(ActiveView.Overview);
323
+ this.controlTabGroup.show(ActiveView.Overview);
188
324
  }
189
325
  }
190
326
  specClicked(event) {
191
327
  this.detailsDrawer.close();
192
- for (let i = 0; i < this.problems?.length; i++) {
193
- if (this.problems[i].startLineNumber === event.detail.line) {
194
- this.problemList.lineClicked(event.detail.line, true);
195
- this.tabGroup.show(ActiveView.Problems);
196
- break;
328
+ if (this.selectedEditorTab == "spec") {
329
+ for (let i = 0; i < this.problems?.length; i++) {
330
+ if (this.problems[i].startLineNumber === event.detail.line) {
331
+ this.problemList.lineClicked(event.detail.line, true);
332
+ this.controlTabGroup.show(ActiveView.Problems);
333
+ if (this.sidebarClosed) {
334
+ this.toggleSidebar();
335
+ }
336
+ break;
337
+ }
338
+ }
339
+ }
340
+ if (this.selectedEditorTab == ActiveView.Ruleset) {
341
+ // check ruleset map for the line number
342
+ const rulesetMap = this.sessionRulesetMapBag?.get(SessionRulesetMapBag);
343
+ if (rulesetMap) {
344
+ for (let i = 0; i < rulesetMap.length; i++) {
345
+ if (rulesetMap[i].line === event.detail.line) {
346
+ this.controlTabGroup.show(ActiveView.Ruleset);
347
+ if (this.sidebarClosed) {
348
+ this.toggleSidebar();
349
+ }
350
+ break;
351
+ }
352
+ }
197
353
  }
198
354
  }
199
355
  }
200
356
  problemClicked(event) {
357
+ if (this.selectedEditorTab != "spec") {
358
+ this.editorTabGroup.show("spec");
359
+ this.selectedEditorTab = "spec";
360
+ }
361
+ this.controlTabGroup.show(ActiveView.Problems);
201
362
  this.editor.editor?.revealLineInCenter(event.detail.problem.line);
202
363
  this.editor.editor?.setPosition({
203
364
  lineNumber: event.detail.problem.line,
@@ -207,11 +368,11 @@ let TheDoctor = class TheDoctor extends LitElement {
207
368
  if (!event.detail.launchedFromProblems) {
208
369
  this.problemList.lineClicked(event.detail.problem.problemObject.startLineNumber);
209
370
  }
210
- this.tabGroup.show(ActiveView.Problems);
211
371
  }
212
372
  loadState() {
213
373
  LintingService.doctorEndpoint = this.doctorEndpoint;
214
374
  FeedbackService.doctorEndpoint = this.doctorEndpoint;
375
+ RulesetService.doctorEndpoint = this.doctorEndpoint;
215
376
  this.docBag = this.bagManager.getBag(DoctorDocumentBag);
216
377
  if (this.docBag) {
217
378
  const doc = this.docBag.get(DefaultDocument);
@@ -240,14 +401,207 @@ let TheDoctor = class TheDoctor extends LitElement {
240
401
  this.problemsOverview.statistics = stats;
241
402
  }
242
403
  }
243
- this.OWASPBag = this.bagManager.getBag(OWASPBag);
244
- if (this.OWASPBag) {
245
- this.OWASPEnabled = this.OWASPBag.get(OWASPBag);
246
- this.owaspSwitch.checked = this.OWASPEnabled;
404
+ // extract rulesets from bags
405
+ const promises = [];
406
+ this.DefaultRulesetBag = this.bagManager.getBag(DefaultRulesetBag);
407
+ if (this.DefaultRulesetBag) {
408
+ const ruleset = this.DefaultRulesetBag.get(DefaultRulesetBag);
409
+ if (ruleset) {
410
+ this.defaultRuleset = ruleset;
411
+ }
412
+ else {
413
+ promises.push(this.fetchDefaultRuleset());
414
+ }
415
+ }
416
+ // OWASP ruleset
417
+ this.OWASPRulesetBag = this.bagManager.getBag(OWASPRulesetBag);
418
+ if (this.OWASPRulesetBag) {
419
+ const ruleset = this.OWASPRulesetBag.get(OWASPRulesetBag);
420
+ if (ruleset) {
421
+ this.OWASPRuleset = ruleset;
422
+ }
423
+ else {
424
+ promises.push(this.fetchOWASPRuleset());
425
+ }
247
426
  }
427
+ // All ruleset
428
+ this.AllRulesetBag = this.bagManager.getBag(AllRulesetBag);
429
+ if (this.AllRulesetBag) {
430
+ const ruleset = this.AllRulesetBag.get(AllRulesetBag);
431
+ if (ruleset) {
432
+ this.AllRuleset = ruleset;
433
+ }
434
+ else {
435
+ promises.push(this.fetchAllRuleset());
436
+ }
437
+ }
438
+ // functions
439
+ this.FunctionsBag = this.bagManager.getBag(FunctionsBag);
440
+ this.FunctionSchemaBag = this.bagManager.getBag(FunctionsSchemaBag);
441
+ if (this.FunctionsBag) {
442
+ const functions = this.FunctionsBag.get(FunctionsBag);
443
+ if (functions) {
444
+ this.functions = functions;
445
+ }
446
+ else {
447
+ promises.push(this.fetchFunctions());
448
+ }
449
+ }
450
+ // custom ruleset
451
+ this.CustomRulesetBag = this.bagManager.getBag(CustomRulesetBag);
452
+ if (this.CustomRulesetBag) {
453
+ const ruleset = this.CustomRulesetBag.get(CustomRulesetBag);
454
+ if (ruleset) {
455
+ this.CustomRuleset = ruleset;
456
+ }
457
+ }
458
+ // create rule configuration bag
459
+ this.RuleConfigBag = this.bagManager.getBag(RuleConfigurationBag);
460
+ // create rule configuration bag
461
+ this.sessionRulesetMapBag = this.bagManager.getBag(SessionRulesetMapBag);
462
+ // fire off all network requests, then configure the ruleset management.
463
+ Promise.all(promises).then(() => {
464
+ // configure rule management
465
+ this.manageRuleset.defaultRuleset = this.defaultRuleset;
466
+ this.manageRuleset.owaspRuleset = this.OWASPRuleset;
467
+ this.manageRuleset.allRuleset = this.AllRuleset;
468
+ this.manageRuleset.functions = this.functions;
469
+ const config = this.RuleConfigBag?.get(RuleConfigurationBag);
470
+ if (config) {
471
+ this.manageRuleset.rulesetConfig = config;
472
+ }
473
+ else {
474
+ this.manageRuleset.rulesetConfig = { ruleMapping: new Map(), allRulesSwitch: true };
475
+ }
476
+ this.manageRuleset.buildRulesets();
477
+ if (this.CustomRuleset && this.CustomRuleset.rules.size > 0) {
478
+ this.manageRuleset.customRuleset = this.CustomRuleset;
479
+ }
480
+ // do nothing with this for now. we just need to make sure the default ruleset
481
+ // has a session.
482
+ this.fetchSessionRulesetAsYaml().then((result) => {
483
+ this.loadingOverlay.hide();
484
+ this.rulesetEditor?.setValue(result, true);
485
+ this.fetchRulesetMap();
486
+ });
487
+ });
248
488
  // refresh state for how to fix.
249
489
  this.fetchDocs();
250
490
  }
491
+ customRulesetEnabled(event) {
492
+ const customRS = { rules: new Map() };
493
+ customRS.id = crypto.randomUUID();
494
+ customRS.owner = this.session.sessionId;
495
+ event.detail.rules.forEach((rule) => {
496
+ if (rule.rule.id != rule.ruleId) {
497
+ rule.rule.id = rule.ruleId;
498
+ }
499
+ customRS.rules.set(rule.ruleId, rule.rule);
500
+ });
501
+ // write state
502
+ this.CustomRulesetBag?.set(CustomRulesetBag, customRS);
503
+ if (event.detail.ruleConfig) {
504
+ this.RuleConfigBag?.set(RuleConfigurationBag, event.detail.ruleConfig);
505
+ }
506
+ else {
507
+ this.RuleConfigBag?.reset();
508
+ // reset session ruleset
509
+ RulesetService.resetSessionRuleset().then(() => {
510
+ this.sendToast({
511
+ id: crypto.randomUUID(),
512
+ type: ToastType.INFO,
513
+ body: "Ruleset has been reset to the default",
514
+ title: "Ruleset reset",
515
+ });
516
+ if (this.docBag) {
517
+ const doc = this.docBag.get(DefaultDocument);
518
+ this.lintSpec(doc);
519
+ // extract bootstrap ruleset
520
+ // LintingService.bootstrapEditor().then((result) => {
521
+ // this.docBag?.set(DefaultDocument, result);
522
+ // this.editor.setValue(result, true);
523
+ // this.lintSpec(doc!);
524
+ // })
525
+ }
526
+ this.fetchSessionRulesetAsYaml().then((result) => {
527
+ this.rulesetEditor?.setValue(result, true);
528
+ });
529
+ }).catch((e) => {
530
+ this.sendToast({
531
+ id: crypto.randomUUID(),
532
+ type: ToastType.ERROR,
533
+ body: e.detail,
534
+ title: "Cannot reset ruleset",
535
+ });
536
+ });
537
+ }
538
+ }
539
+ async fetchSessionRulesetAsYaml() {
540
+ return new Promise(async (resolve) => {
541
+ RulesetService.getSessionRulesetAsYAML().then((result) => {
542
+ resolve(result);
543
+ }).catch((e) => {
544
+ console.error("cannot fetch session ruleset: ", e.title);
545
+ resolve("");
546
+ });
547
+ });
548
+ }
549
+ async fetchFunctions() {
550
+ return new Promise(async (resolve) => {
551
+ RulesetService.getFunctions().then((result) => {
552
+ this.functions = result;
553
+ this.FunctionsBag?.set(FunctionsBag, result);
554
+ // extract schemas for each function
555
+ const promises = [];
556
+ result.forEach((functionId) => {
557
+ promises.push(this.fetchFunctionSchema(functionId));
558
+ });
559
+ Promise.all(promises).then(() => {
560
+ resolve(result);
561
+ });
562
+ });
563
+ });
564
+ }
565
+ async fetchFunctionSchema(functionId) {
566
+ return new Promise(async (resolve) => {
567
+ RulesetService.getFunctionSchema(functionId).then((result) => {
568
+ let m = this.FunctionSchemaBag?.get(FunctionsSchemaBag);
569
+ if (!m) {
570
+ m = new Map();
571
+ }
572
+ m.set(functionId, result);
573
+ this.FunctionSchemaBag?.set(FunctionsSchemaBag, m);
574
+ resolve(result);
575
+ });
576
+ });
577
+ }
578
+ async fetchDefaultRuleset() {
579
+ return new Promise(async (resolve) => {
580
+ RulesetService.getDefaultRuleset().then((result) => {
581
+ this.defaultRuleset = result;
582
+ this.DefaultRulesetBag?.set(DefaultRulesetBag, result);
583
+ resolve(result);
584
+ });
585
+ });
586
+ }
587
+ async fetchOWASPRuleset() {
588
+ return new Promise(async (resolve) => {
589
+ RulesetService.getOWASPRuleset().then((result) => {
590
+ this.OWASPRuleset = result;
591
+ this.OWASPRulesetBag?.set(OWASPRulesetBag, result);
592
+ resolve(result);
593
+ });
594
+ });
595
+ }
596
+ async fetchAllRuleset() {
597
+ return new Promise(async (resolve) => {
598
+ RulesetService.getAllRuleset().then((result) => {
599
+ this.AllRuleset = result;
600
+ this.AllRulesetBag?.set(AllRulesetBag, result);
601
+ resolve(result);
602
+ });
603
+ });
604
+ }
251
605
  fetchDocs() {
252
606
  this.activitySpinner.show();
253
607
  // the first this we need to do is start a session
@@ -268,14 +622,20 @@ let TheDoctor = class TheDoctor extends LitElement {
268
622
  console.error("documentation service is down");
269
623
  });
270
624
  this.ruleDocsBag = this.bagManager.getBag(RuleDocumentationBag);
625
+ this.functionDocsBag = this.bagManager.getBag(FunctionDocumentationBag);
271
626
  // populate docs via worker.
272
627
  this.ruleDocsWorker.addEventListener("message", (event) => {
273
628
  const data = event.data;
274
629
  if (data) {
275
630
  data.forEach((doc) => {
276
- this.ruleDocsBag?.set(doc.ruleId, doc);
277
- this.activitySpinner.hide();
631
+ if (doc.ruleId) {
632
+ this.ruleDocsBag?.set(doc.ruleId, doc);
633
+ }
634
+ if (doc.functionId) {
635
+ this.functionDocsBag?.set(doc.functionId, doc);
636
+ }
278
637
  });
638
+ this.activitySpinner.hide();
279
639
  }
280
640
  });
281
641
  this.ruleDocsWorker.postMessage({ start: true, endpoint: this.doctorEndpoint });
@@ -284,7 +644,117 @@ let TheDoctor = class TheDoctor extends LitElement {
284
644
  console.error("cannot start session");
285
645
  });
286
646
  }
647
+ fetchRulesetMap() {
648
+ RulesetService.getSessionRulesetMap().then((result) => {
649
+ this.sessionRulesetMapBag?.set(SessionRulesetMapBag, result);
650
+ }).catch(() => {
651
+ console.warn("map is empty, or unavailable, Did you submit an empty ruleset?");
652
+ this.sendToast({
653
+ id: crypto.randomUUID(),
654
+ type: ToastType.WARNING,
655
+ body: "map is empty, or unavailable, Did you submit an empty ruleset?",
656
+ title: "Issue fetching ruleset map"
657
+ });
658
+ });
659
+ }
660
+ ruleClicked(evt) {
661
+ // get rule map from bag
662
+ const map = this.sessionRulesetMapBag?.get(SessionRulesetMapBag);
663
+ if (map) {
664
+ const rule = map.find((r) => r.ruleId === evt.detail.ruleId);
665
+ if (rule) {
666
+ this.selectedEditorTab = "ruleset";
667
+ this.editorTabGroup.show(ActiveView.Ruleset);
668
+ this.rulesetEditor.editor?.setPosition({ lineNumber: rule.line, column: rule.start });
669
+ this.rulesetEditor.editor?.revealLineInCenter(rule.line);
670
+ }
671
+ }
672
+ }
673
+ builtInRulesetSelected() {
674
+ // todo
675
+ }
676
+ rulesetManuallyChanged() {
677
+ clearTimeout(this.bounceId);
678
+ this.bounceId = window.setTimeout(() => {
679
+ const ruleset = this.rulesetEditor.editor?.getValue();
680
+ RulesetService.validateRuleset(ruleset).then((result) => {
681
+ // clear any problems
682
+ this.manageRuleset.clearRuleProblems();
683
+ // create a new custom ruleset with a map of rules
684
+ let customRS = { rules: new Map() };
685
+ customRS.id = crypto.randomUUID();
686
+ customRS.owner = this.session.sessionId;
687
+ customRS.rules = new Map(Object.entries(result.rules));
688
+ customRS.rules.forEach((rule, id) => {
689
+ rule.id = id; // set the id to the key
690
+ });
691
+ // write state
692
+ this.CustomRulesetBag?.set(CustomRulesetBag, customRS);
693
+ this.CustomRuleset = customRS;
694
+ if (customRS.rules.size > 0) {
695
+ // rebuild the ruleset management
696
+ this.manageRuleset.customRulesetManual = customRS;
697
+ }
698
+ this.selectedEditorTab = ActiveView.Ruleset;
699
+ this.controlTabGroup.show(ActiveView.Ruleset);
700
+ this.rulesetEditor.setMarkers([]);
701
+ this.fetchRulesetMap();
702
+ }).catch((e) => {
703
+ this.sendToast({
704
+ id: crypto.randomUUID(),
705
+ type: ToastType.ERROR,
706
+ body: e.detail,
707
+ title: e.title
708
+ });
709
+ console.error('unable to validate ruleset', e);
710
+ // process bad rules
711
+ const ruleErrors = e.body;
712
+ const markers = [];
713
+ ruleErrors?.forEach((err) => {
714
+ markers.push({
715
+ severity: MarkerSeverity.Error,
716
+ message: err.ruleError,
717
+ startLineNumber: err.line,
718
+ startColumn: err.startCol,
719
+ endLineNumber: err.line,
720
+ endColumn: err.endCol
721
+ });
722
+ });
723
+ // convert to JSON
724
+ RulesetService.convertToJSON(ruleset).then((json) => {
725
+ // create a new custom ruleset with a map of rules
726
+ let customRS = { rules: new Map() };
727
+ customRS.id = crypto.randomUUID();
728
+ customRS.owner = this.session.sessionId;
729
+ customRS.rules = new Map(Object.entries(json.rules));
730
+ customRS.rules.forEach((rule, id) => {
731
+ if (rule.id != id)
732
+ rule.id = id; // set the id to the key
733
+ });
734
+ this.CustomRuleset = customRS;
735
+ if (customRS.rules.size > 0) {
736
+ // rebuild the ruleset management
737
+ this.manageRuleset.customRulesetManual = customRS;
738
+ }
739
+ this.rulesetEditor.setMarkers([]);
740
+ // clear any problems
741
+ this.manageRuleset.clearRuleProblems();
742
+ // set problems.
743
+ this.rulesetEditor.setMarkers(markers);
744
+ this.manageRuleset.processBadRules(ruleErrors);
745
+ this.controlTabGroup.show(ActiveView.Ruleset);
746
+ }).catch((e) => {
747
+ console.error("unable to convert ruleset to JSON", e);
748
+ });
749
+ });
750
+ }, this.debounceTimeRuleset);
751
+ }
287
752
  specChanged(event) {
753
+ const editor = this.editorMap.get(event.detail.id);
754
+ if (editor && event.detail.id === 'ruleset') {
755
+ this.rulesetManuallyChanged();
756
+ return;
757
+ }
288
758
  if (this.docBag) {
289
759
  this.docBag.set(DefaultDocument, event.detail.content);
290
760
  }
@@ -293,30 +763,114 @@ let TheDoctor = class TheDoctor extends LitElement {
293
763
  this.lintSpec(event.detail.content);
294
764
  }, this.debounceTime);
295
765
  }
296
- toggleOWASP() {
297
- this.OWASPEnabled = this.owaspSwitch.checked;
298
- this.OWASPBag?.set(OWASPBag, this.OWASPEnabled);
299
- if (this.docBag) {
300
- const doc = this.docBag.get(DefaultDocument);
301
- this.lintSpec(doc);
302
- }
303
- }
304
766
  boostrap() {
305
767
  if (this.editor.getValue() === '') {
306
768
  LintingService.bootstrapEditor().then((result) => {
307
769
  this.editor.setValue(result, true);
308
770
  this.specChanged(new CustomEvent(EditorUpdated, {
309
771
  detail: {
310
- content: result
772
+ content: result,
773
+ id: "spec"
311
774
  }
312
775
  }));
313
776
  });
314
777
  }
315
778
  }
779
+ exportJSON() {
780
+ let jsonRS;
781
+ if (this.RuleConfigBag) {
782
+ const config = this.RuleConfigBag.get(RuleConfigurationBag);
783
+ if (config && config.returnedRuleset) {
784
+ jsonRS = config.returnedRuleset;
785
+ }
786
+ }
787
+ if (jsonRS) {
788
+ let reportData = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(jsonRS, null, 2));
789
+ this.downloadRulesetLink.download = "ruleset.json";
790
+ this.downloadRulesetLink.href = reportData;
791
+ this.downloadRulesetLink.click();
792
+ this.exportRulesetDialog.hide();
793
+ }
794
+ }
795
+ exportYAML() {
796
+ RulesetService.getSessionRulesetAsYAML().then((yaml) => {
797
+ let reportData = "data:text/yaml;charset=utf-8," + encodeURIComponent(yaml);
798
+ this.downloadRulesetLink.download = "ruleset.yaml";
799
+ this.downloadRulesetLink.href = reportData;
800
+ this.downloadRulesetLink.click();
801
+ this.exportRulesetDialog.hide();
802
+ });
803
+ }
804
+ confirmExport() {
805
+ switch (this.exportSelection.value) {
806
+ case "json":
807
+ this.exportJSON();
808
+ break;
809
+ case "yaml":
810
+ this.exportYAML();
811
+ break;
812
+ }
813
+ }
316
814
  closeWelcome() {
317
815
  this.welcomeBox.hide();
318
816
  localStorage.setItem("pb33f-doctor-welcome", "closed");
319
817
  }
818
+ selectEditorTab(event) {
819
+ this.selectedEditorTab = event.detail.name;
820
+ switch (event.detail.name) {
821
+ case ActiveView.Ruleset:
822
+ if (this.rulesetPulse) {
823
+ this.rulesetPulse = false;
824
+ }
825
+ this.controlTabGroup.show(ActiveView.Ruleset);
826
+ break;
827
+ }
828
+ }
829
+ selectControlTab(event) {
830
+ switch (event.detail.name) {
831
+ case ActiveView.Problems:
832
+ case ActiveView.Overview:
833
+ if (this.selectedEditorTab != "spec") {
834
+ this.selectedEditorTab = "spec";
835
+ this.editorTabGroup.show("spec");
836
+ }
837
+ break;
838
+ case ActiveView.Ruleset:
839
+ if (this.selectedEditorTab != ActiveView.Ruleset) {
840
+ this.editorTabGroup.show(ActiveView.Ruleset);
841
+ this.selectedEditorTab = ActiveView.Ruleset;
842
+ }
843
+ break;
844
+ }
845
+ }
846
+ toggleSidebar() {
847
+ const splitPanel = this.shadowRoot?.querySelector('sl-split-panel');
848
+ if (!this.sidebarClosed) {
849
+ if (splitPanel) {
850
+ this.collapseButton.name = "chevron-bar-left";
851
+ this.problemsDataDiv.style.display = "none";
852
+ splitPanel.style.setProperty('--min', '40px');
853
+ splitPanel.style.setProperty('--max', 'calc(100vw - 40px)');
854
+ splitPanel.position = 100;
855
+ splitPanel.disabled = true;
856
+ this.splitDivider.style.display = "none";
857
+ this.activitySpinner.classList.add('spinner-draw-closed');
858
+ }
859
+ }
860
+ else {
861
+ if (splitPanel) {
862
+ this.collapseButton.name = "chevron-bar-right";
863
+ splitPanel.style.setProperty('--min', '600px');
864
+ splitPanel.style.setProperty('--max', 'calc(100vw - 600px)');
865
+ splitPanel.position = 60;
866
+ splitPanel.disabled = false;
867
+ this.problemsDataDiv.style.display = "block";
868
+ this.splitDivider.style.display = "block";
869
+ this.activitySpinner.classList.remove('spinner-draw-closed');
870
+ }
871
+ }
872
+ this.sidebarClosed = !this.sidebarClosed;
873
+ }
320
874
  render() {
321
875
  let overlay = html ``;
322
876
  if (this.unavailable) {
@@ -335,48 +889,75 @@ let TheDoctor = class TheDoctor extends LitElement {
335
889
  if (welcomeClosed) {
336
890
  welcomeBox = html ``;
337
891
  }
892
+ let rulesetPulsePill = html ``;
893
+ if (this.rulesetPulse) {
894
+ rulesetPulsePill = html `
895
+ <sl-badge pulse></sl-badge>`;
896
+ }
338
897
  return html `
339
898
  ${welcomeBox}
899
+ ${this.toastManager}
900
+ <sl-dialog open id="loading-overlay" class="dialog-overview"
901
+ style="--width: 30vw" no-header>
902
+ <h3 class="loading">OpenAPI Doctor is booting...</h3>
903
+ </sl-dialog>
904
+ <sl-dialog id="export-ruleset" label="Export Ruleset" class="dialog-overview"
905
+ style="--width: 50vw">
906
+ <div class="export right">
907
+ <div class="export-block">
908
+ <sl-radio-group name="ruleset" value="json" id="export-ruleset-radio">
909
+ <sl-radio-button value="json">JSON
910
+ </sl-radio-button>
911
+ <sl-radio-button value="yaml">YAML
912
+ </sl-radio-button>
913
+ </sl-radio-group>
914
+ </div>
915
+ <div class="export-block">
916
+ <sl-button @click=${this.confirmExport} variant="primary" size="small" style="float: left">
917
+ Export
918
+ </sl-button>
919
+ <a id="download-ruleset" style="display:none"></a>
920
+ </div>
921
+ </div>
922
+ </sl-dialog>
340
923
  <div class="doctor">
341
924
  ${this.activitySpinner}
342
925
  ${this.errorBanner}
343
926
  ${overlay}
344
- <sl-split-panel class="split-panel ${this.unavailable ? 'unavailable' : ''}">
345
- <sl-icon slot="divider" name="grip-vertical"></sl-icon>
927
+ <sl-split-panel class="split-panel ${this.unavailable ? 'unavailable' : ''}" position="60">
928
+ <sl-icon id="split-divider" slot="divider" name="grip-vertical"></sl-icon>
346
929
  <div class="editor" slot="start">
347
930
  ${this.detailsDrawer}
348
- ${this.editor}
931
+ <sl-tab-group class="tab-group" @sl-tab-show="${this.selectEditorTab}" id="editor-controls">
932
+ <sl-tab slot="nav" panel="spec" class="tab" id="spec">OpenAPI Spec</sl-tab>
933
+ <sl-tab slot="nav" panel="ruleset" class="tab" id="ruleset">Ruleset ${rulesetPulsePill}
934
+ </sl-tab>
935
+ <sl-tab-panel name="spec" class="tab-panel">${this.editor}</sl-tab-panel>
936
+ <sl-tab-panel name="ruleset" class="tab-panel">${this.rulesetEditor}</sl-tab-panel>
937
+ </sl-tab-group>
349
938
  </div>
350
939
  <div class="problems" slot="end">
351
- <sl-tab-group class="tab-group ">
352
- <sl-tab slot="nav" panel="overview" class="tab" id="overviewPanel">Overview</sl-tab>
353
- <sl-tab slot="nav" panel="problems" class="tab" id="problemsPanel">Problems</sl-tab>
354
- <sl-tab slot="nav" panel="ruleset" class="tab" id="rulesetPanel">Rules</sl-tab>
355
- <sl-tab slot="nav" panel="feedback" class="tab" id="feedbackPanel">Feedback</sl-tab>
356
- <sl-tab-panel name="overview" class="tab-panel">${this.problemsOverview}</sl-tab-panel>
357
- <sl-tab-panel name="problems" class="tab-panel">${this.problemList}</sl-tab-panel>
358
- <sl-tab-panel name="ruleset" class="tab-panel">
359
- <div class="ruleset">
360
- <pb33f-attention-box type="info" headerText="Manage RuleSets">
361
- Feature coming soon, you will be able to manage your ruleset(s) here.
362
- </pb33f-attention-box>
363
- <sl-switch size="small" @sl-change="${this.toggleOWASP}" id="owasp">Enable <a
364
- href="https://quobix.com/vacuum/rulesets/owasp/">OWASP</a>
365
- Rules</a>?
366
- </sl-switch>
367
- </div>
368
- </sl-tab-panel>
369
- <sl-tab-panel name="feedback" class="tab-panel">
370
- ${this.feedback}
371
- </sl-tab-panel>
372
- </sl-tab-group>
940
+ <div class="problems-data">
941
+ <sl-tab-group class="tab-group" id="manager-controls" @sl-tab-show="${this.selectControlTab}">
942
+ <sl-tab slot="nav" panel="overview" class="tab" id="overviewPanel" active>Overview
943
+ </sl-tab>
944
+ <sl-tab slot="nav" panel="problems" class="tab" id="problemsPanel">Problems</sl-tab>
945
+ <sl-tab slot="nav" panel="ruleset" class="tab" id="rulesetPanel">Rules</sl-tab>
946
+ <sl-tab slot="nav" panel="feedback" class="tab" id="feedbackPanel">Feedback</sl-tab>
947
+ <sl-tab-panel name="overview" class="tab-panel">${this.problemsOverview}</sl-tab-panel>
948
+ <sl-tab-panel name="problems" class="tab-panel">${this.problemList}</sl-tab-panel>
949
+ <sl-tab-panel name="ruleset" class="tab-panel">${this.manageRuleset}</sl-tab-panel>
950
+ <sl-tab-panel name="feedback" class="tab-panel">${this.feedback}</sl-tab-panel>
951
+ </sl-tab-group>
952
+ </div>
953
+ <sl-icon-button class="collapse-side" name="chevron-bar-right" @click="${this.toggleSidebar}"></sl-icon-button>
373
954
  </div>
374
955
  </sl-split-panel>
375
956
  ${this.statusBar}
376
957
  </div>`;
377
958
  }
378
959
  };
379
- TheDoctor.styles = [theDoctorCss, linksCss];
960
+ TheDoctor.styles = [theDoctorCss, linksCss, dialogCss, buttonCss, radioGroupsCss];
380
961
  __decorate([
381
962
  query('#overviewPanel')
382
963
  ], TheDoctor.prototype, "overviewPanel", void 0);
@@ -384,20 +965,44 @@ __decorate([
384
965
  query('#problemsPanel')
385
966
  ], TheDoctor.prototype, "problemsPanel", void 0);
386
967
  __decorate([
387
- query('sl-tab-group')
388
- ], TheDoctor.prototype, "tabGroup", void 0);
968
+ query('sl-tab-group#manager-controls')
969
+ ], TheDoctor.prototype, "controlTabGroup", void 0);
389
970
  __decorate([
390
- query('sl-switch#owasp')
391
- ], TheDoctor.prototype, "owaspSwitch", void 0);
971
+ query('sl-tab-group#editor-controls')
972
+ ], TheDoctor.prototype, "editorTabGroup", void 0);
392
973
  __decorate([
393
974
  query('pb33f-attention-box#welcome')
394
975
  ], TheDoctor.prototype, "welcomeBox", void 0);
976
+ __decorate([
977
+ query('sl-radio-group#export-ruleset-radio')
978
+ ], TheDoctor.prototype, "exportSelection", void 0);
979
+ __decorate([
980
+ query('a#download-ruleset')
981
+ ], TheDoctor.prototype, "downloadRulesetLink", void 0);
982
+ __decorate([
983
+ query('sl-dialog#loading-overlay')
984
+ ], TheDoctor.prototype, "loadingOverlay", void 0);
985
+ __decorate([
986
+ query('div.problems-data')
987
+ ], TheDoctor.prototype, "problemsDataDiv", void 0);
395
988
  __decorate([
396
989
  property({ type: Boolean })
397
990
  ], TheDoctor.prototype, "unavailable", void 0);
398
991
  __decorate([
399
992
  property()
400
993
  ], TheDoctor.prototype, "doctorEndpoint", void 0);
994
+ __decorate([
995
+ query('sl-dialog#export-ruleset')
996
+ ], TheDoctor.prototype, "exportRulesetDialog", void 0);
997
+ __decorate([
998
+ query('sl-icon-button.collapse-side')
999
+ ], TheDoctor.prototype, "collapseButton", void 0);
1000
+ __decorate([
1001
+ query('sl-icon#split-divider')
1002
+ ], TheDoctor.prototype, "splitDivider", void 0);
1003
+ __decorate([
1004
+ state()
1005
+ ], TheDoctor.prototype, "rulesetPulse", void 0);
401
1006
  TheDoctor = __decorate([
402
1007
  customElement("pb33f-doctor")
403
1008
  ], TheDoctor);