@rancher/shell 0.3.8 → 0.3.9

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 (36) hide show
  1. package/assets/translations/en-us.yaml +28 -2
  2. package/babel.config.js +17 -4
  3. package/components/CodeMirror.vue +146 -14
  4. package/components/ContainerResourceLimit.vue +14 -1
  5. package/components/CruResource.vue +21 -5
  6. package/components/ExplorerProjectsNamespaces.vue +5 -1
  7. package/components/GroupPanel.vue +57 -0
  8. package/components/YamlEditor.vue +2 -2
  9. package/components/form/ArrayList.vue +1 -1
  10. package/components/form/KeyValue.vue +34 -1
  11. package/components/form/MatchExpressions.vue +120 -21
  12. package/components/form/NodeAffinity.vue +54 -4
  13. package/components/form/PodAffinity.vue +160 -47
  14. package/components/form/Tolerations.vue +40 -4
  15. package/components/form/__tests__/ArrayList.test.ts +3 -3
  16. package/components/form/__tests__/MatchExpressions.test.ts +1 -1
  17. package/components/nav/Header.vue +2 -0
  18. package/config/settings.ts +6 -1
  19. package/core/plugins-loader.js +0 -2
  20. package/edit/configmap.vue +33 -6
  21. package/edit/provisioning.cattle.io.cluster/AgentConfiguration.vue +326 -0
  22. package/edit/provisioning.cattle.io.cluster/index.vue +1 -0
  23. package/edit/provisioning.cattle.io.cluster/rke2.vue +60 -0
  24. package/mixins/chart.js +1 -1
  25. package/models/batch.cronjob.js +18 -3
  26. package/models/workload.js +1 -1
  27. package/package.json +2 -3
  28. package/pages/auth/login.vue +1 -0
  29. package/pages/prefs.vue +18 -2
  30. package/pkg/vue.config.js +0 -1
  31. package/plugins/codemirror.js +158 -0
  32. package/public/index.html +1 -1
  33. package/types/shell/index.d.ts +20 -1
  34. package/utils/create-yaml.js +105 -8
  35. package/utils/settings.ts +12 -0
  36. package/vue.config.js +2 -2
@@ -1611,6 +1611,27 @@ cluster:
1611
1611
  additionalManifest:
1612
1612
  title: Additional Manifest
1613
1613
  tooltip: 'Additional Kubernetes Manifest YAML to be applied to the cluster on startup.'
1614
+ agentConfig:
1615
+ tabs:
1616
+ cluster: Cluster Agent
1617
+ fleet: Fleet Agent
1618
+ groups:
1619
+ deploymentLabels: Deployment Labels
1620
+ selector: Selector
1621
+ podAffinity: Affinity
1622
+ podTolerations: Tolerations
1623
+ podRequestsAndLimits: Requests and Limits
1624
+ subGroups:
1625
+ podAffinityAnti: Pod Affinity/Anti-Affinity
1626
+ nodeAffinity: Node Affinity
1627
+ banners:
1628
+ advanced: These are advanced configuration options. Generally, they should be left as-is.
1629
+ tolerations: Additional Pod Tolerations will be added to the default Tolerations applied by Rancher.
1630
+ limits: Pod Requests and Limits do not have a default configuration.
1631
+ windowsCompatibility: "We do not recommended removing the Node Affinity rule that prevents the <b>agent</b> from running on Windows nodes as this is not a supported configuration."
1632
+ affinity:
1633
+ default: Use default affinity rules defined by Rancher
1634
+ custom: Use custom affinity rules
1614
1635
  advanced:
1615
1636
  argInfo:
1616
1637
  title: Additional Kubelet Args
@@ -5944,15 +5965,19 @@ workload:
5944
5965
  antiAffinityTitle: Run pods on nodes without pods matching these selectors
5945
5966
  affinityOption: Affinity
5946
5967
  antiAffinityOption: Anti-Affinity
5968
+ matchFields:
5969
+ label: Fields
5947
5970
  matchExpressions:
5971
+ label: Expressions
5948
5972
  addRule: Add Rule
5949
5973
  doesNotExist: is not set
5950
5974
  exists: is set
5951
5975
  greaterThan: ">"
5952
5976
  in: in list
5953
- inNamespaces: "Pods in these namespaces:"
5977
+ inNamespaces: "Specific namespaces"
5954
5978
  key: Key
5955
5979
  lessThan: <
5980
+ matchType: Match Type
5956
5981
  namespaces: Namespaces
5957
5982
  notIn: not in list
5958
5983
  operator: Operator
@@ -5968,6 +5993,7 @@ workload:
5968
5993
  schedulingRules: Run pods on node(s) matching scheduling rules
5969
5994
  specificNode: Run pods on specific node(s)
5970
5995
  thisPodNamespace: This pod's namespace
5996
+ allNamespaces: All namespaces
5971
5997
  topologyKey:
5972
5998
  label: Topology Key
5973
5999
  placeholder: e.g. failure-domain.beta.kubernetes.io/zone
@@ -5995,7 +6021,7 @@ workload:
5995
6021
  effectOptions:
5996
6022
  all: All
5997
6023
  noExecute: NoExecute
5998
- noSchedule: "NoSchedule,"
6024
+ noSchedule: "NoSchedule"
5999
6025
  preferNoSchedule: PreferNoSchedule
6000
6026
  labelKey: Label Key
6001
6027
  operator: Operator
package/babel.config.js CHANGED
@@ -1,5 +1,6 @@
1
- module.exports = {
2
- presets: [
1
+ module.exports = function(api) {
2
+ api.cache(true);
3
+ const presets = [
3
4
  [
4
5
  '@vue/cli-plugin-babel/preset',
5
6
  { useBuiltIns: false }
@@ -8,6 +9,18 @@ module.exports = {
8
9
  '@babel/preset-env',
9
10
  { targets: { node: 'current' } }
10
11
  ]
11
- ],
12
- env: { test: { presets: [['@babel/env', { targets: { node: 'current' } }]] } }
12
+ ];
13
+ const env = { test: { presets: [['@babel/env', { targets: { node: 'current' } }]] } };
14
+
15
+ const plugins = [];
16
+
17
+ if (process.env.NODE_ENV === 'test') {
18
+ plugins.push('transform-require-context');
19
+ }
20
+
21
+ return {
22
+ presets,
23
+ plugins,
24
+ env
25
+ };
13
26
  };
@@ -1,9 +1,18 @@
1
1
  <script>
2
2
  import { KEYMAP } from '@shell/store/prefs';
3
+ import { _EDIT, _VIEW } from '@shell/config/query-params';
3
4
 
4
5
  export default {
5
6
  name: 'CodeMirror',
6
7
  props: {
8
+ /**
9
+ * Sets the edit mode for Text Area.
10
+ * @values _EDIT, _VIEW
11
+ */
12
+ mode: {
13
+ type: String,
14
+ default: _EDIT
15
+ },
7
16
  value: {
8
17
  type: String,
9
18
  required: true,
@@ -11,14 +20,26 @@ export default {
11
20
  options: {
12
21
  type: Object,
13
22
  default: () => {}
23
+ },
24
+ asTextArea: {
25
+ type: Boolean,
26
+ default: false
14
27
  }
15
28
  },
16
29
 
17
30
  data() {
18
- return { loaded: false };
31
+ return {
32
+ codeMirrorRef: null,
33
+ loaded: false
34
+ };
19
35
  },
20
36
 
21
37
  computed: {
38
+
39
+ isDisabled() {
40
+ return this.mode === _VIEW;
41
+ },
42
+
22
43
  combinedOptions() {
23
44
  const theme = this.$store.getters['prefs/theme'];
24
45
  const keymap = this.$store.getters['prefs/get'](KEYMAP);
@@ -39,6 +60,12 @@ export default {
39
60
  showCursorWhenSelecting: true,
40
61
  };
41
62
 
63
+ if (this.asTextArea) {
64
+ out.lineNumbers = false;
65
+ out.tabSize = 0;
66
+ out.extraKeys = { Tab: false };
67
+ }
68
+
42
69
  Object.assign(out, this.options);
43
70
 
44
71
  return out;
@@ -58,35 +85,44 @@ export default {
58
85
  methods: {
59
86
 
60
87
  focus() {
61
- if ( this.$refs.cm ) {
62
- this.$refs.cm.codemirror.focus();
88
+ if ( this.$refs.codeMirrorRef ) {
89
+ this.$refs.codeMirrorRef.codemirror.focus();
63
90
  }
64
91
  },
65
92
 
66
93
  refresh() {
67
- if ( this.$refs.cm ) {
68
- this.$refs.cm.refresh();
94
+ if ( this.$refs.codeMirrorRef ) {
95
+ this.$refs.codeMirrorRef.refresh();
69
96
  }
70
97
  },
71
98
 
72
- onReady(cm) {
99
+ onReady(codeMirrorRef) {
73
100
  this.$nextTick(() => {
74
- cm.refresh();
101
+ codeMirrorRef.refresh();
102
+ this.codeMirrorRef = codeMirrorRef;
75
103
  });
76
- this.$emit('onReady', cm);
104
+ this.$emit('onReady', codeMirrorRef);
77
105
  },
78
106
 
79
107
  onInput(newCode) {
80
108
  this.$emit('onInput', newCode);
81
109
  },
82
110
 
83
- onChanges(cm, changes) {
84
- this.$emit('onChanges', cm, changes);
111
+ onChanges(codeMirrorRef, changes) {
112
+ this.$emit('onChanges', codeMirrorRef, changes);
113
+ },
114
+
115
+ onFocus() {
116
+ this.$emit('onFocus', true);
117
+ },
118
+
119
+ onBlur() {
120
+ this.$emit('onFocus', false);
85
121
  },
86
122
 
87
123
  updateValue(value) {
88
- if ( this.$refs.cm ) {
89
- this.$refs.cm.codemirror.doc.setValue(value);
124
+ if ( this.$refs.codeMirrorRef ) {
125
+ this.$refs.codeMirrorRef.codemirror.doc.setValue(value);
90
126
  }
91
127
  }
92
128
  }
@@ -95,15 +131,21 @@ export default {
95
131
 
96
132
  <template>
97
133
  <client-only placeholder=" Loading...">
98
- <div class="code-mirror">
134
+ <div
135
+ class="code-mirror"
136
+ :class="{['as-text-area']: asTextArea}"
137
+ >
99
138
  <codemirror
100
139
  v-if="loaded"
101
- ref="cm"
140
+ ref="codeMirrorRef"
102
141
  :value="value"
103
142
  :options="combinedOptions"
143
+ :disabled="isDisabled"
104
144
  @ready="onReady"
105
145
  @input="onInput"
106
146
  @changes="onChanges"
147
+ @focus="onFocus"
148
+ @blur="onBlur"
107
149
  />
108
150
  <div v-else>
109
151
  Loading...
@@ -120,5 +162,95 @@ export default {
120
162
  height: initial;
121
163
  background: none
122
164
  }
165
+
166
+ &.as-text-area {
167
+ min-height: 40px;
168
+ position: relative;
169
+ display: block;
170
+ box-sizing: border-box;
171
+ width: 100%;
172
+ padding: 10px;
173
+ background-color: var(--input-bg);
174
+ border-radius: var(--border-radius);
175
+ border: solid var(--border-width) var(--input-border);
176
+ color: var(--input-text);
177
+
178
+ &:hover {
179
+ border-color: var(--input-hover-border);
180
+ }
181
+
182
+ &:focus, &.focus {
183
+ outline: none;
184
+ border-color: var(--outline);
185
+ }
186
+
187
+ .CodeMirror-wrap pre {
188
+ word-break: break-word;
189
+ }
190
+ .CodeMirror-code {
191
+ .CodeMirror-line {
192
+ &:not(:last-child)>span:after,
193
+ .cm-markdown-single-trailing-space-odd:before,
194
+ .cm-markdown-single-trailing-space-even:before {
195
+ color: var(--muted);
196
+ position: absolute;
197
+ line-height: 20px;
198
+ pointer-events: none;
199
+ }
200
+ &:not(:last-child)>span:after {
201
+ content: '↵';
202
+ margin-left: 2px;
203
+ }
204
+ .cm-markdown-single-trailing-space-odd:before,
205
+ .cm-markdown-single-trailing-space-even:before {
206
+ font-weight: bold;
207
+ content: '·';
208
+ }
209
+ }
210
+ }
211
+
212
+ .CodeMirror-lines {
213
+ color: var(--input-text);
214
+ padding: 0;
215
+
216
+ .CodeMirror-line > span > span {
217
+ &.cm-overlay {
218
+ font-family: monospace;
219
+ }
220
+ }
221
+
222
+ .CodeMirror-line > span {
223
+ font-family: $body-font;
224
+ }
225
+ }
226
+
227
+ .CodeMirror-sizer {
228
+ min-height: 20px;
229
+ }
230
+
231
+ .CodeMirror-selected {
232
+ background-color: var(--primary) !important;
233
+ }
234
+
235
+ .CodeMirror-selectedtext {
236
+ color: var(--primary-text);
237
+ }
238
+
239
+ .CodeMirror-line::selection,
240
+ .CodeMirror-line > span::selection,
241
+ .CodeMirror-line > span > span::selection {
242
+ color: var(--primary-text);
243
+ background-color: var(--primary);
244
+ }
245
+
246
+ .CodeMirror-line::-moz-selection,
247
+ .CodeMirror-line > span::-moz-selection,
248
+ .CodeMirror-line > span > span::-moz-selection {
249
+ color: var(--primary-text);
250
+ background-color: var(--primary);
251
+ }
252
+ }
253
+
123
254
  }
255
+
124
256
  </style>
@@ -26,6 +26,11 @@ export default {
26
26
  }
27
27
  },
28
28
 
29
+ handleGpuLimit: {
30
+ type: Boolean,
31
+ default: true
32
+ },
33
+
29
34
  registerBeforeHook: {
30
35
  type: Function,
31
36
  default: null
@@ -180,6 +185,7 @@ export default {
180
185
  :input-exponent="-1"
181
186
  :output-modifier="true"
182
187
  :base-unit="t('suffix.cpus')"
188
+ data-testid="cpu-reservation"
183
189
  @input="updateLimits"
184
190
  />
185
191
  </span>
@@ -192,6 +198,7 @@ export default {
192
198
  :input-exponent="2"
193
199
  :increment="1024"
194
200
  :output-modifier="true"
201
+ data-testid="memory-reservation"
195
202
  @input="updateLimits"
196
203
  />
197
204
  </span>
@@ -207,6 +214,7 @@ export default {
207
214
  :input-exponent="-1"
208
215
  :output-modifier="true"
209
216
  :base-unit="t('suffix.cpus')"
217
+ data-testid="cpu-limit"
210
218
  @input="updateLimits"
211
219
  />
212
220
  </span>
@@ -219,11 +227,15 @@ export default {
219
227
  :input-exponent="2"
220
228
  :increment="1024"
221
229
  :output-modifier="true"
230
+ data-testid="memory-limit"
222
231
  @input="updateLimits"
223
232
  />
224
233
  </span>
225
234
  </div>
226
- <div class="row">
235
+ <div
236
+ v-if="handleGpuLimit"
237
+ class="row"
238
+ >
227
239
  <span class="col span-6">
228
240
  <UnitInput
229
241
  v-model="limitsGpu"
@@ -231,6 +243,7 @@ export default {
231
243
  :label="t('containerResourceLimit.limitsGpu')"
232
244
  :mode="mode"
233
245
  :base-unit="t('suffix.gpus')"
246
+ data-testid="gpu-limit"
234
247
  @input="updateLimits"
235
248
  />
236
249
  </span>
@@ -1,6 +1,6 @@
1
1
  <script>
2
2
  import isEmpty from 'lodash/isEmpty';
3
- import { createYaml } from '@shell/utils/create-yaml';
3
+ import { createYamlWithOptions } from '@shell/utils/create-yaml';
4
4
  import { clone, get } from '@shell/utils/object';
5
5
  import { SCHEMA, NAMESPACE } from '@shell/config/types';
6
6
  import ResourceYaml from '@shell/components/ResourceYaml';
@@ -101,6 +101,11 @@ export default {
101
101
  default: null,
102
102
  },
103
103
 
104
+ preventEnterSubmit: {
105
+ type: Boolean,
106
+ default: false,
107
+ },
108
+
104
109
  applyHooks: {
105
110
  type: Function,
106
111
  default: null,
@@ -141,6 +146,11 @@ export default {
141
146
  description: {
142
147
  type: String,
143
148
  default: ''
149
+ },
150
+
151
+ yamlModifiers: {
152
+ type: Object,
153
+ default: undefined
144
154
  }
145
155
  },
146
156
 
@@ -296,7 +306,7 @@ export default {
296
306
  }
297
307
  },
298
308
 
299
- createResourceYaml() {
309
+ createResourceYaml(modifiers) {
300
310
  const resource = this.resource;
301
311
 
302
312
  if ( typeof this.generateYaml === 'function' ) {
@@ -306,7 +316,7 @@ export default {
306
316
  const schemas = this.$store.getters[`${ inStore }/all`](SCHEMA);
307
317
  const clonedResource = clone(resource);
308
318
 
309
- const out = createYaml(schemas, resource.type, clonedResource);
319
+ const out = createYamlWithOptions(schemas, resource.type, clonedResource, modifiers);
310
320
 
311
321
  return out;
312
322
  }
@@ -317,7 +327,7 @@ export default {
317
327
  await this.applyHooks(BEFORE_SAVE_HOOKS);
318
328
  }
319
329
 
320
- const resourceYaml = this.createResourceYaml();
330
+ const resourceYaml = this.createResourceYaml(this.yamlModifiers);
321
331
 
322
332
  this.resourceYaml = resourceYaml;
323
333
  this.showAsForm = false;
@@ -380,6 +390,12 @@ export default {
380
390
  throw new Error(`Could not create the new namespace. ${ e.message }`);
381
391
  }
382
392
  }
393
+ },
394
+
395
+ onPressEnter(event) {
396
+ if (this.preventEnterSubmit) {
397
+ event.preventDefault();
398
+ }
383
399
  }
384
400
  }
385
401
  };
@@ -398,7 +414,7 @@ export default {
398
414
  :is="(isView? 'div' : 'form')"
399
415
  class="create-resource-container cru__form"
400
416
  @submit.prevent
401
- @keydown.enter.prevent
417
+ @keydown.enter="onPressEnter($event)"
402
418
  >
403
419
  <div
404
420
  v-if="hasErrors"
@@ -11,6 +11,7 @@ import MoveModal from '@shell/components/MoveModal';
11
11
  import { defaultTableSortGenerationFn } from '@shell/components/ResourceTable.vue';
12
12
  import { NAMESPACE_FILTER_ALL_ORPHANS } from '@shell/utils/namespace-filter';
13
13
  import ResourceFetch from '@shell/mixins/resource-fetch';
14
+ import DOMPurify from 'dompurify';
14
15
 
15
16
  export default {
16
17
  name: 'ListProjectNamespace',
@@ -319,7 +320,10 @@ export default {
319
320
  const row = group.rows[0];
320
321
 
321
322
  if (row.isFake) {
322
- return this.t('resourceTable.groupLabel.project', { name: row.project?.nameDisplay }, true);
323
+ return DOMPurify.sanitize(
324
+ this.t('resourceTable.groupLabel.project', { name: row.project?.nameDisplay }, true),
325
+ { ALLOWED_TAGS: ['span'] }
326
+ );
323
327
  }
324
328
 
325
329
  return row.groupByLabel;
@@ -0,0 +1,57 @@
1
+ <script>
2
+ export default {
3
+ props: {
4
+ /**
5
+ * Label for the group
6
+ */
7
+ label: {
8
+ type: String,
9
+ default: null
10
+ },
11
+ /**
12
+ * The i18n key to use for the label
13
+ */
14
+ labelKey: {
15
+ type: String,
16
+ default: null
17
+ },
18
+ }
19
+ };
20
+ </script>
21
+ <template>
22
+ <div class="group-panel-outer">
23
+ <div class="group-panel">
24
+ <div class="group-panel-title">
25
+ <t
26
+ v-if="labelKey"
27
+ :k="labelKey"
28
+ />
29
+ <template v-else-if="label">
30
+ {{ label }}
31
+ </template>
32
+ </div>
33
+ <div class="group-panel-content">
34
+ <slot />
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </template>
39
+
40
+ <style lang="scss" scoped>
41
+ .group-panel {
42
+ border: 1px solid var(--border);
43
+ border-radius: 5px;
44
+ padding: 10px;
45
+ position: relative;
46
+ margin-top: 10px;
47
+ .group-panel-title {
48
+ position: absolute;
49
+ top: -7px;
50
+ background-color: var(--body-bg);
51
+ padding: 0 5px;
52
+ }
53
+ .group-panel-content {
54
+ position: relative;
55
+ }
56
+ }
57
+ </style>
@@ -86,7 +86,7 @@ export default {
86
86
  },
87
87
 
88
88
  computed: {
89
- cmOptions() {
89
+ codeMirrorOptions() {
90
90
  const readOnly = this.editorMode === EDITOR_MODES.VIEW_CODE;
91
91
 
92
92
  const gutters = [];
@@ -228,7 +228,7 @@ export default {
228
228
  ref="cm"
229
229
  :class="{fill: true, scrolling: scrolling}"
230
230
  :value="curValue"
231
- :options="cmOptions"
231
+ :options="codeMirrorOptions"
232
232
  :data-testid="componentTestid + '-code-mirror'"
233
233
  @onInput="onInput"
234
234
  @onReady="onReady"
@@ -224,7 +224,7 @@ export default {
224
224
  <div
225
225
  v-for="(row, idx) in rows"
226
226
  :key="idx"
227
- data-testid="array-list-box"
227
+ :data-testid="`array-list-box${ idx }`"
228
228
  class="box"
229
229
  >
230
230
  <slot
@@ -10,11 +10,13 @@ import Select from '@shell/components/form/Select';
10
10
  import FileSelector from '@shell/components/form/FileSelector';
11
11
  import { _EDIT, _VIEW } from '@shell/config/query-params';
12
12
  import { asciiLike } from '@shell/utils/string';
13
+ import CodeMirror from '@shell/components/CodeMirror';
13
14
 
14
15
  export default {
15
16
  name: 'KeyValue',
16
17
 
17
18
  components: {
19
+ CodeMirror,
18
20
  Select,
19
21
  TextAreaAutoGrow,
20
22
  FileSelector
@@ -139,6 +141,10 @@ export default {
139
141
  type: Boolean,
140
142
  default: false,
141
143
  },
144
+ valueMarkdownMultiline: {
145
+ type: Boolean,
146
+ default: false,
147
+ },
142
148
  valueMultiline: {
143
149
  type: Boolean,
144
150
  default: true,
@@ -245,7 +251,10 @@ export default {
245
251
  data() {
246
252
  const rows = this.getRows(this.value);
247
253
 
248
- return { rows };
254
+ return {
255
+ rows,
256
+ codeMirrorFocus: {},
257
+ };
249
258
  },
250
259
 
251
260
  computed: {
@@ -519,6 +528,19 @@ export default {
519
528
  return this.t('detailText.binary', { n }, true);
520
529
  },
521
530
  get,
531
+ /**
532
+ * Update 'rows' variable with the user's input and prevents to update queue before the row model is updated
533
+ */
534
+ onInputMarkdownMultiline(idx, value) {
535
+ this.rows = this.rows.map((row, i) => i === idx ? { ...row, value } : row);
536
+ this.queueUpdate();
537
+ },
538
+ /**
539
+ * Set focus on CodeMirror fields
540
+ */
541
+ onFocusMarkdownMultiline(idx, value) {
542
+ this.$set(this.codeMirrorFocus, idx, value);
543
+ }
522
544
  }
523
545
  };
524
546
  </script>
@@ -635,6 +657,16 @@ export default {
635
657
  <div v-else-if="row.binary">
636
658
  {{ binaryTextSize(row.value) }}
637
659
  </div>
660
+ <CodeMirror
661
+ v-else-if="valueMarkdownMultiline"
662
+ ref="cm"
663
+ :class="{['focus']: codeMirrorFocus[i]}"
664
+ :value="row[valueName]"
665
+ :as-text-area="true"
666
+ :mode="mode"
667
+ @onInput="onInputMarkdownMultiline(i, $event)"
668
+ @onFocus="onFocusMarkdownMultiline(i, $event)"
669
+ />
638
670
  <TextAreaAutoGrow
639
671
  v-else-if="valueMultiline"
640
672
  v-model="row[valueName]"
@@ -750,6 +782,7 @@ export default {
750
782
  &.value textarea{
751
783
  padding: 10px 10px 10px 10px;
752
784
  }
785
+
753
786
  .text-monospace:not(.conceal) {
754
787
  font-family: monospace, monospace;
755
788
  }