dashboard-shell-shell 3.0.5-test.5 → 3.0.5-test.6

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 (212) hide show
  1. package/assets/brand/harvester/favicon.png +0 -0
  2. package/assets/brand/suse/favicon.png +0 -0
  3. package/assets/images/pl/half-logo.svg +23 -2
  4. package/assets/images/pl/harvester.png +0 -0
  5. package/assets/styles/app.scss +0 -4
  6. package/assets/styles/base/_basic.scss +2 -2
  7. package/assets/styles/base/_mixins.scss +1 -1
  8. package/assets/styles/base/_typography.scss +1 -2
  9. package/assets/styles/base/_variables.scss +4 -11
  10. package/assets/styles/global/_button.scss +25 -43
  11. package/assets/styles/global/_columns.scss +1 -3
  12. package/assets/styles/global/_form.scss +13 -45
  13. package/assets/styles/global/_labeled-input.scss +26 -54
  14. package/assets/styles/global/_layout.scss +3 -8
  15. package/assets/styles/global/_select.scss +17 -25
  16. package/assets/styles/global/_table.scss +1 -7
  17. package/assets/styles/global/_tooltip.scss +8 -56
  18. package/assets/styles/themes/_dark.scss +0 -3
  19. package/assets/styles/themes/_light.scss +43 -66
  20. package/assets/styles/vendor/vue-select.scss +9 -22
  21. package/assets/translations/en-us.yaml +4 -28
  22. package/assets/translations/zh-hans.yaml +189 -452
  23. package/components/ActionDropdown.vue +1 -2
  24. package/components/ActionMenu.vue +2 -2
  25. package/components/ActionMenuShell.vue +0 -2
  26. package/components/AppModal.vue +5 -46
  27. package/components/BrandImage.vue +0 -1
  28. package/components/ButtonDropdown.vue +4 -26
  29. package/components/ButtonMultiAction.vue +0 -1
  30. package/components/ClusterIconMenu.vue +1 -1
  31. package/components/CodeMirror.vue +6 -20
  32. package/components/ConsumptionGauge.vue +5 -24
  33. package/components/CopyToClipboard.vue +0 -15
  34. package/components/CruResource.vue +8 -9
  35. package/components/CruResourceFooter.vue +2 -2
  36. package/components/DashboardOptions.vue +17 -29
  37. package/components/Drawer/Chrome.vue +2 -2
  38. package/components/Drawer/ResourceDetailDrawer/ConfigTab.vue +23 -23
  39. package/components/Drawer/ResourceDetailDrawer/index.vue +2 -2
  40. package/components/ExplorerMembers.vue +3 -18
  41. package/components/ExplorerProjectsNamespaces.vue +5 -19
  42. package/components/GlobalRoleBindings.vue +48 -112
  43. package/components/GrafanaDashboard.vue +4 -4
  44. package/components/GrowlManager.vue +1 -3
  45. package/components/HardwareResourceGauge.vue +3 -39
  46. package/components/IndentedPanel.vue +10 -4
  47. package/components/InfoBox.vue +3 -3
  48. package/components/InputOrDisplay.vue +2 -28
  49. package/components/LabelValue.vue +1 -20
  50. package/components/ModalWithCard.vue +3 -12
  51. package/components/PodSecurityAdmission.vue +1 -1
  52. package/components/PromptModal.vue +1 -1
  53. package/components/PromptRemove.vue +11 -30
  54. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +3 -1
  55. package/components/Resource/Detail/Metadata/KeyValue.vue +4 -8
  56. package/components/Resource/Detail/Metadata/index.vue +1 -2
  57. package/components/Resource/Detail/TitleBar/Title.vue +3 -4
  58. package/components/Resource/Detail/TitleBar/index.vue +24 -103
  59. package/components/ResourceDetail/Masthead/legacy.vue +38 -181
  60. package/components/ResourceDetail/legacy.vue +14 -32
  61. package/components/ResourceList/Masthead.vue +54 -226
  62. package/components/ResourceList/ResourceLoadingIndicator.vue +2 -5
  63. package/components/ResourceTable.vue +2 -24
  64. package/components/SideNav.vue +20 -74
  65. package/components/SortableTable/THead.vue +3 -33
  66. package/components/SortableTable/index.vue +464 -1017
  67. package/components/SortableTable/paging.js +16 -26
  68. package/components/SortableTable/selection.js +2 -2
  69. package/components/Tabbed/Tab.vue +3 -3
  70. package/components/Tabbed/index.vue +30 -48
  71. package/components/YamlEditor.vue +1 -0
  72. package/components/auth/Principal.vue +13 -37
  73. package/components/auth/RoleDetailEdit.vue +7 -58
  74. package/components/auth/SelectPrincipal.vue +0 -1
  75. package/components/form/ArrayList.vue +33 -41
  76. package/components/form/ArrayListGrouped.vue +2 -10
  77. package/components/form/ArrayListSelect.vue +1 -1
  78. package/components/form/BannerSettings.vue +59 -64
  79. package/components/form/ChangePassword.vue +4 -4
  80. package/components/form/ColorInput.vue +8 -32
  81. package/components/form/Footer.vue +8 -11
  82. package/components/form/InputWithSelect.vue +5 -8
  83. package/components/form/KeyValue.vue +7 -47
  84. package/components/form/LabeledSelect.vue +242 -214
  85. package/components/form/Labels.vue +3 -3
  86. package/components/form/MatchExpressions.vue +7 -24
  87. package/components/form/Members/ClusterPermissionsEditor.vue +2 -1
  88. package/components/form/Members/MembershipEditor.vue +1 -1
  89. package/components/form/NameNsDescription.vue +20 -59
  90. package/components/form/Password.vue +7 -16
  91. package/components/form/PodAffinity.vue +5 -4
  92. package/components/form/ResourceQuota/Namespace.vue +4 -4
  93. package/components/form/ResourceQuota/NamespaceRow.vue +17 -18
  94. package/components/form/ResourceQuota/Project.vue +4 -4
  95. package/components/form/ResourceQuota/ProjectRow.vue +6 -3
  96. package/components/form/Select.vue +2 -5
  97. package/components/form/SimpleSecretSelector.vue +9 -29
  98. package/components/form/Taints.vue +1 -2
  99. package/components/form/UnitInput.vue +3 -8
  100. package/components/form/WorkloadPorts.vue +123 -143
  101. package/components/formatter/BadgeStateFormatter.vue +5 -8
  102. package/components/formatter/LiveDate.vue +3 -3
  103. package/components/nav/Favorite.vue +1 -5
  104. package/components/nav/Group.vue +99 -132
  105. package/components/nav/Header.vue +27 -124
  106. package/components/nav/HeaderPageActionMenu.vue +0 -1
  107. package/components/nav/NamespaceFilter.vue +15 -19
  108. package/components/nav/TopLevelMenu.vue +119 -182
  109. package/components/nav/Type.vue +41 -63
  110. package/composables/useClickOutside.ts +1 -1
  111. package/config/private-label.js +11 -15
  112. package/config/product/auth.js +7 -17
  113. package/config/product/settings.js +9 -19
  114. package/config/settings.ts +0 -28
  115. package/config/table-headers.js +2 -3
  116. package/dialog/ForceMachineRemoveDialog.vue +2 -2
  117. package/dialog/ScalePoolDownDialog.vue +2 -2
  118. package/edit/management.cattle.io.user.vue +4 -17
  119. package/edit/monitoring.coreos.com.alertmanagerconfig/auth.vue +19 -19
  120. package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +31 -31
  121. package/edit/monitoring.coreos.com.alertmanagerconfig/routeConfig.vue +12 -36
  122. package/edit/monitoring.coreos.com.alertmanagerconfig/types/email.vue +6 -6
  123. package/edit/monitoring.coreos.com.alertmanagerconfig/types/opsgenie.vue +10 -10
  124. package/edit/monitoring.coreos.com.alertmanagerconfig/types/pagerduty.vue +4 -4
  125. package/edit/monitoring.coreos.com.alertmanagerconfig/types/slack.vue +4 -4
  126. package/edit/monitoring.coreos.com.alertmanagerconfig/types/webhook.vue +1 -1
  127. package/edit/monitoring.coreos.com.receiver/types/email.vue +6 -6
  128. package/edit/monitoring.coreos.com.receiver/types/opsgenie.vue +10 -10
  129. package/edit/monitoring.coreos.com.receiver/types/pagerduty.vue +5 -5
  130. package/edit/monitoring.coreos.com.receiver/types/slack.vue +4 -4
  131. package/edit/namespace.vue +2 -1
  132. package/edit/token.vue +12 -31
  133. package/edit/workload/index.vue +4 -4
  134. package/list/management.cattle.io.setting.vue +13 -22
  135. package/list/management.cattle.io.user.vue +3 -7
  136. package/list/namespace.vue +0 -3
  137. package/list/provisioning.cattle.io.cluster.vue +7 -6
  138. package/mixins/brand.js +0 -17
  139. package/package.json +1 -1
  140. package/pages/account/index.vue +25 -79
  141. package/pages/auth/login.vue +52 -220
  142. package/pages/auth/setup.vue +19 -142
  143. package/pages/c/_cluster/_product/namespaces.vue +5 -5
  144. package/pages/c/_cluster/auth/roles/index.vue +1 -19
  145. package/pages/c/_cluster/monitoring/monitor/index.vue +2 -2
  146. package/pages/c/_cluster/settings/banners.vue +102 -174
  147. package/pages/c/_cluster/settings/brand.vue +302 -350
  148. package/pages/c/_cluster/settings/performance.vue +38 -61
  149. package/pages/home.vue +30 -70
  150. package/pages/prefs.vue +25 -27
  151. package/promptRemove/mixin/roleDeletionCheck.js +2 -2
  152. package/public/index.html +4 -4
  153. package/rancher-components/BadgeState/BadgeState.vue +55 -38
  154. package/rancher-components/Banner/Banner.vue +8 -12
  155. package/rancher-components/Card/Card.vue +8 -7
  156. package/rancher-components/Form/Checkbox/Checkbox.vue +0 -4
  157. package/rancher-components/Form/LabeledInput/LabeledInput.vue +3 -42
  158. package/rancher-components/Form/Radio/RadioButton.vue +11 -35
  159. package/rancher-components/Form/Radio/RadioGroup.vue +5 -13
  160. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +3 -3
  161. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +0 -1
  162. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +4 -12
  163. package/rancher-components/RcDropdown/RcDropdown.vue +7 -35
  164. package/rancher-components/RcDropdown/RcDropdownItem.vue +2 -2
  165. package/rancher-components/RcDropdown/RcDropdownMenu.vue +6 -12
  166. package/rancher-components/RcDropdown/types.ts +0 -1
  167. package/rancher-components/StringList/StringList.vue +1 -1
  168. package/static/favicon.ico +0 -0
  169. package/static/favicon.png +0 -0
  170. package/static/loading-indicator.html +3 -3
  171. package/store/i18n.js +2 -5
  172. package/store/modal.ts +3 -3
  173. package/store/prefs.js +4 -11
  174. package/store/type-map.js +2 -32
  175. package/types/shell/index.d.ts +67 -74
  176. package/utils/error.js +8 -87
  177. package/utils/router.js +0 -21
  178. package/utils/select.js +3 -26
  179. package/utils/string.js +5 -8
  180. package/utils/title.ts +1 -1
  181. package/assets/icons/demo.css +0 -539
  182. package/assets/icons/demo.css:Zone.Identifier +0 -0
  183. package/assets/icons/demo_index.html +0 -1131
  184. package/assets/icons/demo_index.html:Zone.Identifier +0 -0
  185. package/assets/icons/iconfont.css +0 -216
  186. package/assets/icons/iconfont.css:Zone.Identifier +0 -0
  187. package/assets/icons/iconfont.js +0 -1
  188. package/assets/icons/iconfont.js:Zone.Identifier +0 -0
  189. package/assets/icons/iconfont.json +0 -324
  190. package/assets/icons/iconfont.json:Zone.Identifier +0 -0
  191. package/assets/icons/iconfont.ttf +0 -0
  192. package/assets/icons/iconfont.ttf:Zone.Identifier +0 -0
  193. package/assets/icons/iconfont.woff +0 -0
  194. package/assets/icons/iconfont.woff2 +0 -0
  195. package/assets/icons/iconfont.woff2:Zone.Identifier +0 -0
  196. package/assets/icons/iconfont.woff:Zone.Identifier +0 -0
  197. package/assets/images/API.svg +0 -3
  198. package/assets/images/action.svg +0 -6
  199. package/assets/images/login/password.svg +0 -20
  200. package/assets/images/login/user.svg +0 -6
  201. package/assets/images/login-bg.png +0 -0
  202. package/assets/images/login-left.png +0 -0
  203. package/assets/images/login-logo.svg +0 -19
  204. package/assets/images/logo.png +0 -0
  205. package/assets/images/pl/logo.png +0 -0
  206. package/assets/images/promp-yellow.svg +0 -5
  207. package/assets/images/user.png +0 -0
  208. package/assets/styles/all.scss +0 -63
  209. package/components/DotState.vue +0 -84
  210. package/components/breadcrumb/index.vue +0 -340
  211. package/pages/account/pri.vue +0 -229
  212. package/utils/errorTranslate.json +0 -1336
package/utils/error.js CHANGED
@@ -1,7 +1,5 @@
1
1
  import { isArray } from '@shell/utils/array';
2
2
 
3
- import translations from './errorTranslate.json';
4
-
5
3
  export class ClusterNotFoundError extends Error {
6
4
  static NAME = 'ClusterNotFoundError'
7
5
 
@@ -50,6 +48,7 @@ export class ApiError extends Error {
50
48
 
51
49
  export function stringify(err) {
52
50
  let str;
51
+
53
52
  if ( typeof err === 'string' ) {
54
53
  str = err;
55
54
  } else if ( err && typeof err === 'object' ) {
@@ -61,7 +60,7 @@ export function stringify(err) {
61
60
  const data = JSON.parse(str).data;
62
61
 
63
62
  if (data) {
64
- return translateError(data);
63
+ return data;
65
64
  }
66
65
  } catch {}
67
66
  }
@@ -85,7 +84,7 @@ export function stringify(err) {
85
84
  str = JSON.stringify(err);
86
85
  }
87
86
 
88
- return translateError(str);
87
+ return str;
89
88
  }
90
89
 
91
90
  export function exceptionToErrorsArray(err) {
@@ -93,21 +92,16 @@ export function exceptionToErrorsArray(err) {
93
92
  const body = err.response.data;
94
93
 
95
94
  if ( body && body.message ) {
96
- return [translateError(body.message)];
95
+ return [body.message];
97
96
  } else {
98
- return [translateError(err)];
97
+ return [err];
99
98
  }
100
99
  } else if (err.status && err.message) {
101
- return [translateError(err.message)];
100
+ return [err.message];
102
101
  } else if ( isArray(err) ) {
103
- let arr = []
104
- for (let index = 0; index < err.length; index++) {
105
- arr[index] = translateError(err[index])
106
- }
107
-
108
- return [...new Set(arr)];
102
+ return err;
109
103
  } else {
110
- return [translateError(err)];
104
+ return [err];
111
105
  }
112
106
  }
113
107
 
@@ -135,76 +129,3 @@ export const normalizeError = (err) => {
135
129
  statusCode: (err.statusCode || err.status || (err.response && err.response.status) || 500)
136
130
  };
137
131
  };
138
- export function translateError(error) {
139
- error = typeof error === 'string'
140
- ? error
141
- : error?.message || error?.toString() || '';
142
-
143
- const originError = error;
144
- error = error?.toLowerCase().replace(/admission webhook(.*?)denied the request:/g, (match, p1) => {
145
- return '';
146
- }).replace('internal error occurred: ', "");
147
- console.log('00044', error);
148
-
149
-
150
- if (error.includes('spec and status of disks on node') && error.includes('are being syncing and please retry later')) {
151
- error = error.replace('spec and status of disks on node','').replace('are being syncing and please retry later','')
152
- error = '节点' + error + '上的硬盘规格和状态正在同步,请稍后重试'
153
- } else if (error.includes('same granularity as schedule') && error.includes('and offset less than')) {
154
- error = error.replace('same granularity as schedule','').replace('and offset less than 10m0s','')
155
- error = '与' + error + '的时间间隔应大于10分钟'
156
- } else if (error.includes('should be greater than or equal to') && error.includes('but got')) {
157
- error.replace('should be greater than or equal to','应该大于等于').replace('but got','但是填写的是')
158
- }else if(error.includes('e.g')){
159
- error = error.replace(/\s*(\(\s*)?e\.g\..*$/, '')
160
- } else if(error.includes("because it's still used by vm(s)")) {
161
- error = error.replace(/because it's still used by vm\(s\)/i, "因为正在被虚拟机使用:");
162
- } else if (
163
- error.includes('subnet.kubeovn.io') &&
164
- error.includes('metadata.name in body should match') &&
165
- error.includes('spec.excludeips[') &&
166
- error.includes('in body should match')
167
- ) {
168
- // 提取子网名与索引
169
- const subnetMatch = error.match(/subnet\.kubeovn\.io\s+['"]([^'"]+)['"]/);
170
- const ipMatch = error.match(/spec\.excludeips\[(\d+)\]/);
171
-
172
- const subnetName = subnetMatch ? subnetMatch[1] : '';
173
- const idx = ipMatch ? ipMatch[1] : '0';
174
-
175
- error = `子网 '${subnetName}' 配置无效:1) 名称禁止以数字开头;2) 排除IP[${idx}] 不能为空且必须是合法 IPv4/IPv6 地址或地址段`;
176
- }
177
-
178
- // error = error.replace(/admission webhook(.*?)denied the request:/g, (match, p1) => {
179
- // return '';
180
- // })
181
- // .replace(/cannot unmarshal number into(.*?)of type string/g, (match, p1) => {
182
- // return '不能数字开头和纯数字';
183
- // })
184
- // Apply translations from JSON
185
- if(error.indexOf('err:') !== -1){
186
- const errorIndex = error.indexOf('err:')
187
- error = error.substring(errorIndex + 4);
188
- }
189
-
190
-
191
- for (const translation of translations.translations) {
192
-
193
- // if (error.includes(translation.pattern)) {
194
- // error = error.replace(translation.pattern, translation.replacement);
195
- // }
196
- const regex = new RegExp(translation.pattern, translation.flags || 'g');
197
- // error = error.replace(regex, translation.replacement);
198
- const matches = error.match(regex);
199
-
200
- if (matches) {
201
- if (matches.length == 1) {
202
- error = error.replace(regex, translation.replacement);
203
- } else {
204
- error = translation.pattern.replace(/\$(.+)/g, (_, index) => matches[parseInt(index)]);
205
- }
206
- }
207
- }
208
-
209
- return error;
210
- }
package/utils/router.js CHANGED
@@ -1,26 +1,5 @@
1
1
  import { INSTALL_REDIRECT_META_KEY } from '@shell/config/router/navigation-guards/install-redirect';
2
2
 
3
- export function harvesterhci2cloud(str) {
4
- return str?.replace('harvesterhci.io', 'cloudhci.io')
5
- }
6
-
7
- export function cloud2harvesterhci(str) {
8
- // console.log('00099', str)
9
- // const regex = /[a-z]+cloud.io.(.+)/;
10
- // if (str?.match(regex)) {
11
- // return str;
12
- // }
13
- return str?.replace('cloudhci.io', 'harvesterhci.io')
14
- }
15
-
16
- export function harvester2cloud(str) {
17
- return str?.replace('harvester', 'cloud')
18
- }
19
-
20
- export function cloud2harvester(str) {
21
- return str?.replace('cloud', 'harvester')
22
- }
23
-
24
3
  export function queryParamsFor(current, qp, defaults = {}) {
25
4
  const query = Object.assign({}, current || {});
26
5
 
package/utils/select.js CHANGED
@@ -26,19 +26,8 @@ export function onClickOption(option, e) {
26
26
  // This is a simpler positionner for the dropdown for a select control
27
27
  // We used to use popper for these, but it does not suppotr fractional pixel placements which
28
28
  // means the dropdown does not appear aligned to the control when placed in a column-based layout
29
- export function calculatePosition(dropdownList, component, width, placement, type, isLabel,leftWidth) {
29
+ export function calculatePosition(dropdownList, component, width, placement) {
30
30
  const selectEl = component.$parent.$el;
31
-
32
- const labeledContainer = selectEl.querySelector('.labeled-container');
33
- let labelWidth=0
34
- if (labeledContainer) {
35
- labelWidth= labeledContainer.offsetWidth;
36
- } else {
37
- console.warn("未找到类名为 labeled-container 的元素");
38
- }
39
-
40
-
41
-
42
31
  const r = selectEl.getBoundingClientRect();
43
32
  const p = placement || 'bottom-start';
44
33
  const docHeight = document.body.offsetHeight;
@@ -70,19 +59,7 @@ export function calculatePosition(dropdownList, component, width, placement, typ
70
59
  selectEl.classList.remove('vs__dropdown-up');
71
60
  }
72
61
 
73
- // dropdownList.style.left = `${ r.x }px`;
62
+ dropdownList.style.left = `${ r.x }px`;
74
63
  dropdownList.style.width = 'min-content';
75
- // const s = type === 'label'? `${ r.x }px` :`${ r.x + Number(type.split("px")[0]) }px`
76
- dropdownList.style.left = ((type === 'label') && (!isLabel)) ? `${ (r.x) + labelWidth }px` : `${ r.x + (typeof type === 'string' ? parseFloat(type) : 0) }px`;
77
- // dropdownList.style.minWidth = type === 'label' ? `${ (r.width) - 160 }px` : `${ r.width }px`;
78
- console.log(type);
79
- console.log( r.width);
80
- if(leftWidth!=='') {
81
- dropdownList.style.left = leftWidth
82
- }
83
- const SelectContainer = selectEl.querySelector('.labeled-select');
84
-
85
-
86
- // dropdownList.style.width = type === 'label' ? '400px' : `${ r.width }px`;
87
- dropdownList.style.width = type === 'label' ? `${SelectContainer.offsetWidth}px` : `${ r.width- (typeof type === 'string' ? parseFloat(type) : 0) }px`;
64
+ dropdownList.style.minWidth = `${ r.width }px`;
88
65
  }
package/utils/string.js CHANGED
@@ -1,5 +1,3 @@
1
- import { translateError } from "@shell/utils/error";
2
-
3
1
  export function camelToTitle(str) {
4
2
  return dasherize((str || '')).split('-').map((str) => {
5
3
  return ucFirst(str);
@@ -163,19 +161,19 @@ export function resourceNames(names, plusMore, t, endString) {
163
161
 
164
162
  // endString default value
165
163
  if (!endString) {
166
- endString = endString === false ? ' ' : '';
164
+ endString = endString === false ? ' ' : '.';
167
165
  }
168
166
 
169
167
  return names.reduce((res, name, i) => {
170
168
  if (i < MAX_NAMES_COUNT) {
171
- res += `<span>${ escapeHtml( name ) }</span>`;
169
+ res += `<b>${ escapeHtml( name ) }</b>`;
172
170
 
173
171
  if (i === names.length - 1) {
174
172
  res += endString;
175
173
  } else if (i === names.length - 2) {
176
- res += names.length <= 5 ? '' : '';
174
+ res += names.length <= 5 ? t('generic.and') : '';
177
175
  } else {
178
- res += i < MAX_NAMES_COUNT - 1 ? ' , ' : '';
176
+ res += i < MAX_NAMES_COUNT - 1 ? t('generic.comma') : '';
179
177
  }
180
178
  }
181
179
 
@@ -284,8 +282,7 @@ export function ensureRegex(strOrRegex, exact = true) {
284
282
  }
285
283
 
286
284
  export function nlToBr(value) {
287
-
288
- return translateError(escapeHtml(value || '').replace(/(\r\n|\r|\n)/g, '<br/>\n'));
285
+ return escapeHtml(value || '').replace(/(\r\n|\r|\n)/g, '<br/>\n');
289
286
  }
290
287
 
291
288
  const quotedMatch = /[^."']+|"([^"]*)"|'([^']*)'/g;
package/utils/title.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export function updatePageTitle(...breadcrumb: (string | null | undefined | false)[]): void {
2
- document.title = breadcrumb.filter((s) => s).join(' - ').replace(/<i[^>]*>|<\/i>/g, '').replace('Harvester', 'HCI');
2
+ document.title = breadcrumb.filter((s) => s).join(' - ');
3
3
  }