@pb33f/cowboy-components 0.1.15 → 0.2.0

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