@rancher/shell 0.1.2 → 0.1.3

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 (258) hide show
  1. package/assets/translations/en-us.yaml +27 -769
  2. package/assets/translations/zh-hans.yaml +8 -769
  3. package/components/ActionMenu.vue +3 -3
  4. package/components/CodeMirror.vue +6 -8
  5. package/components/CommunityLinks.vue +1 -1
  6. package/components/ContainerResourceLimit.vue +14 -0
  7. package/components/ExplorerMembers.vue +123 -0
  8. package/components/ExplorerProjectsNamespaces.vue +405 -0
  9. package/components/GrafanaDashboard.vue +17 -2
  10. package/components/LocaleSelector.vue +81 -0
  11. package/components/PromptModal.vue +2 -3
  12. package/components/ResourceList/index.vue +1 -1
  13. package/components/ResourceTable.vue +3 -6
  14. package/components/SingleClusterInfo.vue +1 -1
  15. package/components/SortableTable/index.vue +23 -20
  16. package/components/SortableTable/selection.js +1 -0
  17. package/components/auth/AzureWarning.vue +5 -1
  18. package/components/auth/Principal.vue +1 -1
  19. package/components/auth/RoleDetailEdit.vue +18 -11
  20. package/components/fleet/FleetRepos.vue +0 -2
  21. package/components/form/NameNsDescription.vue +4 -6
  22. package/components/form/NodeScheduling.vue +1 -1
  23. package/components/form/WorkloadPorts.vue +1 -1
  24. package/components/formatter/WorkloadHealthScale.vue +1 -1
  25. package/components/nav/Header.vue +9 -9
  26. package/components/nav/NamespaceFilter.vue +7 -4
  27. package/components/nav/TopLevelMenu.vue +6 -43
  28. package/components/nav/WindowManager/ContainerLogs.vue +1 -1
  29. package/config/product/harvester-manager.js +64 -2
  30. package/config/product/manager.js +9 -0
  31. package/config/settings.js +17 -71
  32. package/config/table-headers.js +0 -1
  33. package/config/types.js +5 -25
  34. package/core/plugin-routes.ts +34 -22
  35. package/core/plugin.ts +15 -3
  36. package/core/plugins-loader.js +2 -0
  37. package/core/plugins.js +79 -36
  38. package/core/types.ts +7 -1
  39. package/detail/provisioning.cattle.io.cluster.vue +13 -0
  40. package/detail/workload/index.vue +11 -5
  41. package/{components/dialog → dialog}/AddClusterMemberDialog.vue +0 -0
  42. package/{components/dialog → dialog}/AddCustomBadgeDialog.vue +0 -0
  43. package/{components/dialog → dialog}/AddProjectMemberDialog.vue +0 -0
  44. package/{components/dialog → dialog}/AddonConfigConfirmationDialog.vue +0 -0
  45. package/{components/dialog → dialog}/DrainNode.vue +0 -0
  46. package/{components/dialog → dialog}/ForceMachineRemoveDialog.vue +0 -0
  47. package/{components/dialog → dialog}/GenericPrompt.vue +0 -0
  48. package/{components/dialog → dialog}/RollbackWorkloadDialog.vue +0 -0
  49. package/{components/dialog → dialog}/RotateCertificatesDialog.vue +0 -0
  50. package/{components/dialog → dialog}/RotateEncryptionKeyDialog.vue +0 -0
  51. package/{components/dialog → dialog}/SaveAsRKETemplateDialog.vue +0 -0
  52. package/{components/dialog → dialog}/ScaleMachineDownDialog.vue +0 -0
  53. package/edit/auth/azuread.vue +20 -1
  54. package/edit/management.cattle.io.project.vue +2 -2
  55. package/edit/namespace.vue +17 -10
  56. package/edit/persistentvolumeclaim.vue +1 -0
  57. package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +1 -1
  58. package/edit/provisioning.cattle.io.cluster/MachinePool.vue +33 -5
  59. package/edit/service.vue +1 -1
  60. package/edit/workload/index.vue +363 -15
  61. package/edit/workload/mixins/workload.js +62 -7
  62. package/edit/workload/storage/persistentVolumeClaim/persistentvolumeclaim.vue +1 -0
  63. package/layouts/default.vue +52 -27
  64. package/layouts/error.vue +5 -1
  65. package/layouts/home.vue +6 -2
  66. package/list/harvesterhci.io.management.cluster.vue +74 -33
  67. package/list/namespace.vue +3 -5
  68. package/machine-config/amazonec2.vue +2 -0
  69. package/machine-config/harvester.vue +96 -49
  70. package/middleware/authenticated.js +56 -52
  71. package/mixins/form-validation.js +1 -1
  72. package/mixins/resource-fetch.js +3 -1
  73. package/models/fleet.cattle.io.bundle.js +26 -19
  74. package/models/harvesterhci.io.management.cluster.js +194 -5
  75. package/models/management.cattle.io.cluster.js +1 -1
  76. package/models/management.cattle.io.clusterroletemplatebinding.js +9 -0
  77. package/models/management.cattle.io.project.js +23 -2
  78. package/models/namespace.js +19 -3
  79. package/models/pod.js +19 -2
  80. package/models/provisioning.cattle.io.cluster.js +4 -0
  81. package/models/workload.js +4 -243
  82. package/models/workload.service.js +314 -0
  83. package/nuxt.config.js +11 -9
  84. package/package.json +3 -3
  85. package/pages/auth/login.vue +11 -2
  86. package/pages/auth/setup.vue +1 -1
  87. package/pages/c/_cluster/_product/members/index.vue +3 -93
  88. package/pages/c/_cluster/_product/projectsnamespaces.vue +6 -403
  89. package/pages/c/_cluster/settings/performance.vue +19 -16
  90. package/pages/fail-whale.vue +1 -10
  91. package/pages/index.vue +18 -4
  92. package/pages/plugins.vue +2 -2
  93. package/pages/prefs.vue +8 -6
  94. package/pkg/auto-import.js +44 -7
  95. package/pkg/dynamic-plugin-loader.js +28 -0
  96. package/pkg/import.js +2 -2
  97. package/pkg/model-loader-require.lib.js +3 -0
  98. package/pkg/vue.config.js +9 -6
  99. package/plugins/dashboard-store/model-loader-require.js +12 -0
  100. package/plugins/dashboard-store/model-loader.js +4 -1
  101. package/plugins/dashboard-store/resource-class.js +10 -3
  102. package/plugins/steve/actions.js +1 -1
  103. package/plugins/steve/index.js +6 -4
  104. package/plugins/steve/subscribe.js +34 -23
  105. package/rancher-components/Form/Checkbox/Checkbox.test.ts +77 -0
  106. package/rancher-components/Form/Checkbox/Checkbox.vue +12 -2
  107. package/scripts/build-pkg.sh +48 -2
  108. package/scripts/drone-build-pkg.sh +31 -0
  109. package/scripts/publish-shell.sh +10 -11
  110. package/scripts/serve-pkgs +17 -10
  111. package/store/catalog.js +3 -1
  112. package/store/i18n.js +16 -11
  113. package/store/index.js +4 -181
  114. package/store/prefs.js +30 -2
  115. package/store/type-map.js +16 -29
  116. package/utils/cluster.js +1 -1
  117. package/utils/custom-validators.js +1 -12
  118. package/utils/dynamic-importer.js +1 -1
  119. package/utils/validators/setting.js +0 -35
  120. package/components/FilterLabel.vue +0 -254
  121. package/components/HarvesterUpgradeProgressBarList.vue +0 -109
  122. package/components/VMConsoleBar.vue +0 -87
  123. package/components/dialog/harvester/AddHotplugModal.vue +0 -159
  124. package/components/dialog/harvester/BackupModal.vue +0 -117
  125. package/components/dialog/harvester/CloneTemplate.vue +0 -125
  126. package/components/dialog/harvester/EjectCDROMDialog.vue +0 -157
  127. package/components/dialog/harvester/ExportImageDialog.vue +0 -152
  128. package/components/dialog/harvester/MaintenanceDialog.vue +0 -94
  129. package/components/dialog/harvester/MigrationDialog.vue +0 -154
  130. package/components/dialog/harvester/RestoreDialog.vue +0 -153
  131. package/components/dialog/harvester/SupportBundle.vue +0 -217
  132. package/components/dialog/harvester/UnplugVolume.vue +0 -108
  133. package/components/form/SerialConsole/index.vue +0 -267
  134. package/components/formatter/AttachVMWithName.vue +0 -46
  135. package/components/formatter/CloudInitType.vue +0 -27
  136. package/components/formatter/HarvesterBackupTargetValidation.vue +0 -43
  137. package/components/formatter/HarvesterCPUUsed.vue +0 -122
  138. package/components/formatter/HarvesterDiskState.vue +0 -66
  139. package/components/formatter/HarvesterHostName.vue +0 -66
  140. package/components/formatter/HarvesterIpAddress.vue +0 -90
  141. package/components/formatter/HarvesterMemoryUsed.vue +0 -140
  142. package/components/formatter/HarvesterMigrationState.vue +0 -85
  143. package/components/formatter/HarvesterNodeName.vue +0 -49
  144. package/components/formatter/HarvesterStorageUsed.vue +0 -194
  145. package/components/formatter/HarvesterVmState.vue +0 -123
  146. package/components/nav/HarvesterUpgrade.vue +0 -232
  147. package/components/novnc/NovncConsole.vue +0 -93
  148. package/components/novnc/NovncConsoleItem.vue +0 -89
  149. package/components/novnc/NovncConsoleWrapper.vue +0 -243
  150. package/config/harvester-map.js +0 -44
  151. package/config/harvester-table-headers.js +0 -27
  152. package/config/product/harvester.js +0 -305
  153. package/detail/harvesterhci.io.host/HarvesterHostBasic.vue +0 -364
  154. package/detail/harvesterhci.io.host/HarvesterHostDisk.vue +0 -200
  155. package/detail/harvesterhci.io.host/HarvesterHostNetwork.vue +0 -89
  156. package/detail/harvesterhci.io.host/VirtualMachineInstance.vue +0 -134
  157. package/detail/harvesterhci.io.host/index.vue +0 -243
  158. package/detail/harvesterhci.io.virtualmachinebackup/index.vue +0 -221
  159. package/detail/harvesterhci.io.virtualmachineimage.vue +0 -118
  160. package/detail/kubevirt.io.virtualmachine/VirtualMachineTabs/VirtualMachineBasics.vue +0 -279
  161. package/detail/kubevirt.io.virtualmachine/VirtualMachineTabs/VirtualMachineEvents.vue +0 -75
  162. package/detail/kubevirt.io.virtualmachine/VirtualMachineTabs/VirtualMachineKeypairs.vue +0 -114
  163. package/detail/kubevirt.io.virtualmachine/VirtualMachineTabs/VirtualMachineMigration.vue +0 -79
  164. package/detail/kubevirt.io.virtualmachine/index.vue +0 -213
  165. package/edit/harvesterhci.io.cloudtemplate.vue +0 -123
  166. package/edit/harvesterhci.io.host/HarvesterDisk.vue +0 -262
  167. package/edit/harvesterhci.io.host/index.vue +0 -533
  168. package/edit/harvesterhci.io.keypair.vue +0 -112
  169. package/edit/harvesterhci.io.managedchart/index.vue +0 -25
  170. package/edit/harvesterhci.io.managedchart/rancher-monitoring.vue +0 -172
  171. package/edit/harvesterhci.io.networkattachmentdefinition.vue +0 -210
  172. package/edit/harvesterhci.io.setting/additional-ca.vue +0 -36
  173. package/edit/harvesterhci.io.setting/backup-target.vue +0 -182
  174. package/edit/harvesterhci.io.setting/http-proxy.vue +0 -79
  175. package/edit/harvesterhci.io.setting/index.vue +0 -201
  176. package/edit/harvesterhci.io.setting/overcommit-config.vue +0 -94
  177. package/edit/harvesterhci.io.setting/ssl-certificates.vue +0 -117
  178. package/edit/harvesterhci.io.setting/ssl-parameters.vue +0 -161
  179. package/edit/harvesterhci.io.setting/support-bundle-image.vue +0 -134
  180. package/edit/harvesterhci.io.setting/support-bundle-namespaces.vue +0 -73
  181. package/edit/harvesterhci.io.setting/vip-pools.vue +0 -244
  182. package/edit/harvesterhci.io.setting/vm-force-reset-policy.vue +0 -81
  183. package/edit/harvesterhci.io.virtualmachinebackup.vue +0 -256
  184. package/edit/harvesterhci.io.virtualmachineimage.vue +0 -364
  185. package/edit/harvesterhci.io.virtualmachinetemplateversion.vue +0 -340
  186. package/edit/harvesterhci.io.volume.vue +0 -195
  187. package/edit/kubevirt.io.virtualmachine/VirtualMachineAccessCredentials/AccessCredentialsUsers.vue +0 -190
  188. package/edit/kubevirt.io.virtualmachine/VirtualMachineAccessCredentials/index.vue +0 -212
  189. package/edit/kubevirt.io.virtualmachine/VirtualMachineAccessCredentials/type/basicAuth.vue +0 -94
  190. package/edit/kubevirt.io.virtualmachine/VirtualMachineAccessCredentials/type/sshkey.vue +0 -85
  191. package/edit/kubevirt.io.virtualmachine/VirtualMachineCloudConfig/DataTemplate.vue +0 -153
  192. package/edit/kubevirt.io.virtualmachine/VirtualMachineCloudConfig/index.vue +0 -279
  193. package/edit/kubevirt.io.virtualmachine/VirtualMachineCpuMemory.vue +0 -113
  194. package/edit/kubevirt.io.virtualmachine/VirtualMachineNetwork/__tests__/HarvesterEditNetwork.test.ts +0 -41
  195. package/edit/kubevirt.io.virtualmachine/VirtualMachineNetwork/base.vue +0 -281
  196. package/edit/kubevirt.io.virtualmachine/VirtualMachineNetwork/index.vue +0 -142
  197. package/edit/kubevirt.io.virtualmachine/VirtualMachineReserved.vue +0 -54
  198. package/edit/kubevirt.io.virtualmachine/VirtualMachineSSHKey.vue +0 -256
  199. package/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/index.vue +0 -391
  200. package/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/__tests__/HarvesterEditContainer.test.ts +0 -40
  201. package/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/__tests__/HarvesterEditExisting.test.ts +0 -102
  202. package/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/__tests__/HarvesterEditVMImage.test.ts +0 -117
  203. package/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/__tests__/HarvesterEditVolume.test.ts +0 -74
  204. package/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/container.vue +0 -132
  205. package/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/existing.vue +0 -303
  206. package/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/vmImage.vue +0 -285
  207. package/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/volume.vue +0 -188
  208. package/edit/kubevirt.io.virtualmachine/index.vue +0 -642
  209. package/edit/network.harvesterhci.io.clusternetwork/index.vue +0 -19
  210. package/edit/network.harvesterhci.io.clusternetwork/vlan.vue +0 -134
  211. package/edit/workload/types/Deployment.vue +0 -377
  212. package/edit/workload/types/Generic.vue +0 -295
  213. package/list/harvesterhci.io.cloudtemplate.vue +0 -78
  214. package/list/harvesterhci.io.dashboard/HarvesterUpgrade.vue +0 -211
  215. package/list/harvesterhci.io.dashboard/UpgradeInfo.vue +0 -40
  216. package/list/harvesterhci.io.dashboard/index.vue +0 -752
  217. package/list/harvesterhci.io.host/index.vue +0 -186
  218. package/list/harvesterhci.io.networkattachmentdefinition.vue +0 -167
  219. package/list/harvesterhci.io.setting.vue +0 -241
  220. package/list/harvesterhci.io.virtualmachinebackup.vue +0 -172
  221. package/list/harvesterhci.io.virtualmachineimage.vue +0 -80
  222. package/list/harvesterhci.io.virtualmachinetemplateversion.vue +0 -173
  223. package/list/harvesterhci.io.volume.vue +0 -122
  224. package/list/kubevirt.io.virtualmachine.vue +0 -193
  225. package/mixins/harvester-vm/impl.js +0 -267
  226. package/mixins/harvester-vm/index.js +0 -1357
  227. package/models/harvester/configmap.js +0 -32
  228. package/models/harvester/harvesterhci.io.blockdevice.js +0 -55
  229. package/models/harvester/harvesterhci.io.keypair.js +0 -12
  230. package/models/harvester/harvesterhci.io.setting.js +0 -127
  231. package/models/harvester/harvesterhci.io.supportbundle.js +0 -35
  232. package/models/harvester/harvesterhci.io.upgrade.js +0 -226
  233. package/models/harvester/harvesterhci.io.virtualmachinebackup.js +0 -116
  234. package/models/harvester/harvesterhci.io.virtualmachineimage.js +0 -255
  235. package/models/harvester/harvesterhci.io.virtualmachinerestore.js +0 -43
  236. package/models/harvester/harvesterhci.io.virtualmachinetemplate.js +0 -69
  237. package/models/harvester/harvesterhci.io.virtualmachinetemplateversion.js +0 -227
  238. package/models/harvester/k8s.cni.cncf.io.networkattachmentdefinition.js +0 -32
  239. package/models/harvester/kubevirt.io.virtualmachine.js +0 -850
  240. package/models/harvester/kubevirt.io.virtualmachineinstance.js +0 -142
  241. package/models/harvester/management.cattle.io.managedchart.js +0 -191
  242. package/models/harvester/management.cattle.io.setting.js +0 -40
  243. package/models/harvester/network.harvesterhci.io.clusternetwork.js +0 -100
  244. package/models/harvester/network.harvesterhci.io.nodenetwork.js +0 -34
  245. package/models/harvester/node.js +0 -255
  246. package/models/harvester/persistentvolumeclaim.js +0 -166
  247. package/models/harvester/pod.js +0 -185
  248. package/pages/c/_cluster/harvester/airgapupgrade/index.vue +0 -309
  249. package/pages/c/_cluster/harvester/console/_uid/serial.vue +0 -51
  250. package/pages/c/_cluster/harvester/console/_uid/vnc.vue +0 -52
  251. package/pages/c/_cluster/harvester/index.vue +0 -24
  252. package/pages/c/_cluster/harvester/support/index.vue +0 -154
  253. package/pkg/model-loader.lib.js +0 -3
  254. package/promptRemove/kubevirt.io.virtualmachine.vue +0 -164
  255. package/store/harvester-common.js +0 -126
  256. package/utils/validators/vm-datavolumes.js +0 -38
  257. package/utils/validators/vm-image.js +0 -32
  258. package/utils/validators/vm.js +0 -221
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const contextFolders = ['chart', 'cloud-credential', 'content', 'detail', 'edit', 'list', 'machine-config', 'models', 'promptRemove', 'l10n', 'windowComponents', 'formatters'];
3
+ const contextFolders = ['chart', 'cloud-credential', 'content', 'detail', 'edit', 'list', 'machine-config', 'models', 'promptRemove', 'l10n', 'windowComponents', 'dialog', 'formatters'];
4
4
  const contextMap = contextFolders.reduce((map, obj) => {
5
5
  map[obj] = true;
6
6
 
@@ -11,6 +11,19 @@ function replaceAll(str, find, replace) {
11
11
  return str.split(find).join(replace);
12
12
  }
13
13
 
14
+ function registerFile(file, type, pkg, f) {
15
+ const importType = (f === 'models') ? 'require' : 'import';
16
+ const chunkName = (f === 'l10n') ? '' : `/* webpackChunkName: "${ f }" */`;
17
+
18
+ return ` $plugin.register('${ f }', '${ type }', () => ${ importType }(${ chunkName }'${ pkg }/${ f }/${ file }'));\n`;
19
+ }
20
+
21
+ function register(file, pkg, f) {
22
+ const name = file.replace(/\.[^/.]+$/, '');
23
+
24
+ return registerFile(file, name, pkg, f);
25
+ }
26
+
14
27
  // This function is used to generate the code to register models, edit, detail, list etc for a type
15
28
  // This is used when building as a library - it does not use require.context - it scans the file system and build time.
16
29
  // This ensures that the webpackChunkName is respected (require.context does not support this) - so when build as a library
@@ -20,13 +33,34 @@ function generateTypeImport(pkg, dir) {
20
33
 
21
34
  // Auto-import if the folder exists
22
35
  contextFolders.forEach((f) => {
23
- if (fs.existsSync(path.join(dir, f))) {
36
+ const filePath = path.join(dir, f);
37
+
38
+ if (fs.existsSync(filePath)) {
24
39
  fs.readdirSync(path.join(dir, f)).forEach((file) => {
25
- const name = file.replace(/\.[^/.]+$/, '');
26
- const importType = (f === 'models') ? 'require' : 'import';
27
- const chunkName = (f === 'l10n') ? '' : `/* webpackChunkName: "${ f }" */`;
40
+ const fileStat = fs.lstatSync(path.join(filePath, file));
41
+
42
+ // Directories are special cases
43
+ if (fileStat.isDirectory()) {
44
+ // This might be a <type>/index.vue (aka nested component)
45
+ const indexFilePath = path.join(file, 'index.vue');
46
+ const fullIndexFilePath = path.join(filePath, indexFilePath);
28
47
 
29
- content += ` $plugin.register('${ f }', '${ name }', () => ${ importType }(${ chunkName }'${ pkg }/${ f }/${ file }'));\n`;
48
+ if (fs.existsSync(fullIndexFilePath)) {
49
+ content += registerFile(indexFilePath, file, pkg, f);
50
+
51
+ return;
52
+ }
53
+
54
+ // This might be a <store name>/<model name|type>.js file (aka nested model)
55
+ if (f === 'models') {
56
+ fs.readdirSync(path.join(filePath, file)).forEach((store) => {
57
+ content += register(path.join(file, store), pkg, f);
58
+ });
59
+ }
60
+ } else {
61
+ // This is a simple <resource type>.<file type> file
62
+ content += register(file, pkg, f);
63
+ }
30
64
  });
31
65
  }
32
66
  });
@@ -48,7 +82,10 @@ function generateDynamicTypeImport(pkg, dir) {
48
82
  // Auto-import if the folder exists
49
83
  contextFolders.forEach((f) => {
50
84
  if (fs.existsSync(path.join(dir, f))) {
51
- let genImport = replaceAll(template, 'NAME', f);
85
+ const safeName = f.replace(/\/|-/g, '_');
86
+ let genImport = replaceAll(template, 'NAME', safeName);
87
+
88
+ genImport = replaceAll(genImport, 'DIR', f );
52
89
  const importType = (f === 'models') ? 'require' : 'import';
53
90
  // Ensure i18n chunks are named with the request name (which will be the locale)
54
91
  const chunk = (f === 'l10n') ? '[request]' : f;
@@ -0,0 +1,28 @@
1
+
2
+ /**
3
+ * Some plugins won't be bundled with the dashboard build but loaded on demand at run time.
4
+ * This file allows 'manager' style plugins to defined how to determine if a unknown route
5
+ * belongs to one of their associated plugins and how that plugin can be loaded
6
+ */
7
+ class DynamicPluginLoader {
8
+ dynamicPluginLoaders = [];
9
+
10
+ register(reg) {
11
+ this.dynamicPluginLoaders.push(reg);
12
+ }
13
+
14
+ async check({ route, store }) {
15
+ for (const dpl of this.dynamicPluginLoaders) {
16
+ // Check that the route is valid and then load the plugin associated with it
17
+ const res = await dpl.load({ route, store });
18
+
19
+ if (res) {
20
+ return res;
21
+ }
22
+ }
23
+ }
24
+ }
25
+
26
+ const dynamicPluginLoader = new DynamicPluginLoader();
27
+
28
+ export default dynamicPluginLoader;
package/pkg/import.js CHANGED
@@ -1,4 +1,4 @@
1
- const _NAME = require.context('BASE/NAME', true, /\.(vue|js|yaml)$/).keys();
1
+ const _NAME = require.context('BASE/DIR', true, /\.(vue|js|yaml)$/).keys();
2
2
 
3
3
  _NAME.forEach((f) => {
4
4
  let name = f.substr(2);
@@ -6,5 +6,5 @@ _NAME.forEach((f) => {
6
6
 
7
7
  name = name.substr(0, ext);
8
8
 
9
- $plugin.register('NAME', name, () => REQUIRE(CHUNK`BASE/NAME/${ name }EXT`)); // eslint-disable-line no-undef
9
+ $plugin.register('DIR', name, () => REQUIRE(CHUNK`BASE/DIR/${ name }EXT`)); // eslint-disable-line no-undef
10
10
  });
@@ -0,0 +1,3 @@
1
+ export default function modelLoaderRequire(type) {
2
+ return null;
3
+ }
package/pkg/vue.config.js CHANGED
@@ -46,20 +46,23 @@ module.exports = function(dir) {
46
46
  config.resolve.alias['~pkg'] = dir;
47
47
  delete config.resolve.alias['@'];
48
48
 
49
- // Prevent the dynamic importer and the model-loader from importing anything dynamically - we don't want all of the
49
+ // Prevent the dynamic importer and the model-loader-require from importing anything dynamically - we don't want all of the
50
50
  // models etc when we build as a library
51
- const dynamicImporterOveride = new webpack.NormalModuleReplacementPlugin(/dynamic-importer$/, (resource) => {
51
+ const dynamicImporterOverride = new webpack.NormalModuleReplacementPlugin(/dynamic-importer$/, (resource) => {
52
52
  resource.request = path.join(__dirname, 'dynamic-importer.lib.js');
53
53
  });
54
- const modelLoaderImporterOveride = new webpack.NormalModuleReplacementPlugin(/model-loader$/, (resource) => {
55
- resource.request = path.join(__dirname, 'model-loader.lib.js');
54
+ const modelLoaderImporterOverride = new webpack.NormalModuleReplacementPlugin(/model-loader-require$/, (resource) => {
55
+ const fileName = 'model-loader-require.lib.js';
56
+ const pkgModelLoaderRequire = path.join(dir, fileName);
57
+
58
+ resource.request = fs.existsSync(pkgModelLoaderRequire) ? pkgModelLoaderRequire : path.join(__dirname, fileName);
56
59
  });
57
60
 
58
61
  // Auto-generate module to import the types (model, detail, edit etc)
59
62
  const autoImportPlugin = new VirtualModulesPlugin({ 'node_modules/@rancher/auto-import': generateTypeImport('@pkg', dir) });
60
63
 
61
- config.plugins.unshift(dynamicImporterOveride);
62
- config.plugins.unshift(modelLoaderImporterOveride);
64
+ config.plugins.unshift(dynamicImporterOverride);
65
+ config.plugins.unshift(modelLoaderImporterOverride);
63
66
  config.plugins.unshift(autoImportPlugin);
64
67
  // config.plugins.unshift(debug);
65
68
 
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Fetch a model from the dashboard's local model's folder
3
+ *
4
+ * Splitting this out into a separate function means packages can
5
+ * override this to ensure dashboard models aren't bundled with it
6
+ *
7
+ * @param {string} type
8
+ * @returns Model for the given type
9
+ */
10
+ export default function modelLoaderRequire(type) {
11
+ return require(`@shell/models/${ type }`);
12
+ }
@@ -1,4 +1,5 @@
1
1
  import { normalizeType } from './normalize';
2
+ import modelLoaderRequire from './model-loader-require';
2
3
 
3
4
  const cache = {};
4
5
 
@@ -16,7 +17,9 @@ function find(cache, type, rootState) {
16
17
  let base;
17
18
 
18
19
  if (!pluginModel) {
19
- base = require(`@shell/models/${ type }`);
20
+ // Model hasn't come from a plugin, fall back on something reasonable
21
+ // by default this is the dashboard's local models folder
22
+ base = modelLoaderRequire(type);
20
23
  } else if (typeof pluginModel === 'function') {
21
24
  // pluginModel could be an object in the case the plugin is built-in
22
25
  base = pluginModel();
@@ -24,7 +24,7 @@ import {
24
24
  validateDnsLikeTypes,
25
25
  validateLength,
26
26
  } from '@shell/utils/validators';
27
- import formRulesGenerator from '@shell/utils/validators/formRules';
27
+ import formRulesGenerator from '@shell/utils/validators/formRules/index';
28
28
  import jsyaml from 'js-yaml';
29
29
  import compact from 'lodash/compact';
30
30
  import forIn from 'lodash/forIn';
@@ -1587,8 +1587,15 @@ export default class Resource {
1587
1587
  if (!isEmpty(validatorName) && validatorExists) {
1588
1588
  CustomValidators[validatorName](pathValue, this.$rootGetters, errors, validatorArgs, displayKey, data);
1589
1589
  } else if (!isEmpty(validatorName) && !validatorExists) {
1590
- // eslint-disable-next-line
1591
- console.warn(this.t('validation.custom.missing', { validatorName }));
1590
+ // Check if validator is imported from plugin
1591
+ const pluginValidator = this.$rootState.$plugin?.getValidator(validatorName);
1592
+
1593
+ if (pluginValidator) {
1594
+ pluginValidator(pathValue, this.$rootGetters, errors, validatorArgs, displayKey, data);
1595
+ } else {
1596
+ // eslint-disable-next-line
1597
+ console.warn(this.t('validation.custom.missing', { validatorName }));
1598
+ }
1592
1599
  }
1593
1600
  });
1594
1601
  });
@@ -178,7 +178,7 @@ export default {
178
178
  const res = err.response;
179
179
 
180
180
  // Go to the logout page for 401s, unless redirectUnauthorized specifically disables (for the login page)
181
- if ( opt.redirectUnauthorized !== false && process.client && res.status === 401 ) {
181
+ if ( opt.redirectUnauthorized !== false && res.status === 401 ) {
182
182
  dispatch('auth/logout', opt.logoutOnError, { root: true });
183
183
  }
184
184
 
@@ -10,7 +10,7 @@ import getters, { STEVE_MODEL_TYPES } from './getters';
10
10
  import mutations from './mutations';
11
11
  import actions from './actions';
12
12
 
13
- function SteveFactory(namespace, baseUrl) {
13
+ export function SteveFactory(namespace, baseUrl) {
14
14
  return {
15
15
  ...coreStoreModule,
16
16
 
@@ -50,6 +50,10 @@ function SteveFactory(namespace, baseUrl) {
50
50
  };
51
51
  }
52
52
 
53
+ export const steveStoreInit = (store, ctx) => {
54
+ createWorker(store, ctx);
55
+ };
56
+
53
57
  export default (config) => {
54
58
  config.namespace = config.namespace || '';
55
59
 
@@ -67,8 +71,6 @@ export default (config) => {
67
71
  return coreStore(
68
72
  SteveFactory(config.namespace, config.baseUrl),
69
73
  config,
70
- (store, ctx) => {
71
- createWorker(store, ctx);
72
- }
74
+ steveStoreInit
73
75
  );
74
76
  };
@@ -1,6 +1,7 @@
1
1
  import { addObject, clear, removeObject } from '@shell/utils/array';
2
2
  import { get } from '@shell/utils/object';
3
- import { COUNT, SCHEMA } from '@shell/config/types';
3
+ import { COUNT, MANAGEMENT, SCHEMA } from '@shell/config/types';
4
+ import { SETTING } from '@shell/config/settings';
4
5
  import Socket, {
5
6
  EVENT_CONNECTED,
6
7
  EVENT_DISCONNECTED,
@@ -456,28 +457,38 @@ export const actions = {
456
457
  clearTimeout(state.queueTimer);
457
458
  state.queueTimer = null;
458
459
  if (e.type === EVENT_DISCONNECT_ERROR) {
459
- // do not send a growl notification unless the socket stays disconnected for more than MINIMUM_TIME_DISCONNECTED
460
- setTimeout(() => {
461
- if (state.socket.isConnected()) {
462
- return;
463
- }
464
- const dateFormat = escapeHtml( rootGetters['prefs/get'](DATE_FORMAT));
465
- const timeFormat = escapeHtml( rootGetters['prefs/get'](TIME_FORMAT));
466
- const time = e?.srcElement?.disconnectedAt || Date.now();
467
-
468
- const timeFormatted = `${ day(time).format(`${ dateFormat } ${ timeFormat }`) }`;
469
- const url = e?.srcElement?.url;
470
-
471
- const t = rootGetters['i18n/t'];
472
-
473
- dispatch('growl/error', {
474
- title: t('growl.disconnected.title'),
475
- message: t('growl.disconnected.message', { url, time: timeFormatted }, { raw: true }),
476
- icon: 'error',
477
- earliestClose: time + MINIMUM_TIME_NOTIFIED + MINIMUM_TIME_DISCONNECTED,
478
- url
479
- }, { root: true });
480
- }, MINIMUM_TIME_DISCONNECTED);
460
+ // determine if websocket notifications are disabled
461
+ const perfSetting = rootGetters['management/byId'](MANAGEMENT.SETTING, SETTING.UI_PERFORMANCE);
462
+ let disableGrowl = false;
463
+
464
+ if ( perfSetting?.value ) {
465
+ disableGrowl = JSON.parse(perfSetting.value).disableWebsocketNotification || false;
466
+ }
467
+
468
+ if ( !disableGrowl ) {
469
+ // do not send a growl notification unless the socket stays disconnected for more than MINIMUM_TIME_DISCONNECTED
470
+ setTimeout(() => {
471
+ if (state.socket.isConnected()) {
472
+ return;
473
+ }
474
+ const dateFormat = escapeHtml( rootGetters['prefs/get'](DATE_FORMAT));
475
+ const timeFormat = escapeHtml( rootGetters['prefs/get'](TIME_FORMAT));
476
+ const time = e?.srcElement?.disconnectedAt || Date.now();
477
+
478
+ const timeFormatted = `${ day(time).format(`${ dateFormat } ${ timeFormat }`) }`;
479
+ const url = e?.srcElement?.url;
480
+
481
+ const t = rootGetters['i18n/t'];
482
+
483
+ dispatch('growl/error', {
484
+ title: t('growl.disconnected.title'),
485
+ message: t('growl.disconnected.message', { url, time: timeFormatted }, { raw: true }),
486
+ icon: 'error',
487
+ earliestClose: time + MINIMUM_TIME_NOTIFIED + MINIMUM_TIME_DISCONNECTED,
488
+ url
489
+ }, { root: true });
490
+ }, MINIMUM_TIME_DISCONNECTED);
491
+ }
481
492
  } else {
482
493
  // if the error is not a disconnect error, the socket never worked: log whether the current browser is safari
483
494
  console.error(`WebSocket Connection Error [${ getters.storeName }]`, e.detail); // eslint-disable-line no-console
@@ -0,0 +1,77 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import { Checkbox } from './index';
3
+
4
+ describe('Checkbox.vue', () => {
5
+ it('is unchecked by default', () => {
6
+ const wrapper = shallowMount(Checkbox);
7
+ const cbInput = wrapper.find('input[type="checkbox"]').element as HTMLInputElement;
8
+
9
+ expect(cbInput.checked).toBe(false);
10
+ });
11
+
12
+ it('renders a true value', () => {
13
+ const wrapper = shallowMount(Checkbox, { propsData: { value: true } });
14
+ const cbInput = wrapper.find('input[type="checkbox"]').element as HTMLInputElement;
15
+
16
+ expect(cbInput.checked).toBe(true);
17
+ });
18
+
19
+ it('updates from false to true when props change', async () => {
20
+ const wrapper = shallowMount(Checkbox);
21
+ const cbInput = wrapper.find('input[type="checkbox"]').element as HTMLInputElement;
22
+
23
+ expect(cbInput.checked).toBe(false);
24
+
25
+ await wrapper.setProps({ value: true });
26
+
27
+ expect(cbInput.checked).toBe(true);
28
+ });
29
+
30
+ it('emits an input event with a true value', async () => {
31
+ const wrapper = shallowMount(Checkbox);
32
+ const event = {
33
+ target: { tagName: 'input', href: null },
34
+ stopPropagation: () => { },
35
+ preventDefault: () => { }
36
+ };
37
+
38
+ (wrapper.vm as any).clicked(event);
39
+ await wrapper.vm.$nextTick();
40
+
41
+ expect(wrapper.emitted().input?.length).toBe(1);
42
+ expect(wrapper.emitted().input?.[0][0]).toBe(true);
43
+ });
44
+
45
+ it('emits an input event with a custom valueWhenTrue', async () => {
46
+ const valueWhenTrue = 'BIG IF TRUE';
47
+ const event = {
48
+ target: { tagName: 'input', href: null },
49
+ stopPropagation: () => { },
50
+ preventDefault: () => { }
51
+ };
52
+
53
+ const wrapper = shallowMount(Checkbox, { propsData: { value: false, valueWhenTrue } });
54
+
55
+ (wrapper.vm as any).clicked(event);
56
+ await wrapper.vm.$nextTick();
57
+
58
+ expect(wrapper.emitted().input?.length).toBe(1);
59
+ expect(wrapper.emitted().input?.[0][0]).toBe(valueWhenTrue);
60
+ });
61
+
62
+ it('updates from valueWhenTrue to falsy', async () => {
63
+ const valueWhenTrue = 'REAL HUGE IF FALSE';
64
+ const event = {
65
+ target: { tagName: 'input', href: null },
66
+ stopPropagation: () => { },
67
+ preventDefault: () => { }
68
+ };
69
+
70
+ const wrapper = shallowMount(Checkbox, { propsData: { value: valueWhenTrue, valueWhenTrue } });
71
+
72
+ (wrapper.vm as any).clicked(event);
73
+ await wrapper.vm.$nextTick();
74
+
75
+ expect(wrapper.emitted().input?.[0][0]).toBe(null);
76
+ })
77
+ });
@@ -9,7 +9,7 @@ export default Vue.extend({
9
9
  * The checkbox value.
10
10
  */
11
11
  value: {
12
- type: [Boolean, Array] as PropType<boolean | boolean[]>,
12
+ type: [Boolean, Array, String] as PropType<boolean | boolean[] | string>,
13
13
  default: false
14
14
  },
15
15
 
@@ -169,6 +169,12 @@ export default Vue.extend({
169
169
  addObject(this.value, this.valueWhenTrue);
170
170
  }
171
171
  this.$emit('input', this.value);
172
+ } else if (this.isString(this.valueWhenTrue)) {
173
+ if (this.isChecked) {
174
+ this.$emit('input', null);
175
+ } else {
176
+ this.$emit('input', this.valueWhenTrue);
177
+ }
172
178
  } else {
173
179
  this.$emit('input', !this.value);
174
180
  this.$el.dispatchEvent(click);
@@ -178,10 +184,14 @@ export default Vue.extend({
178
184
  /**
179
185
  * Determines if there are multiple values for the checkbox.
180
186
  */
181
- isMulti(value: boolean | boolean[]): value is boolean[] {
187
+ isMulti(value: boolean | boolean[] | string): value is boolean[] {
182
188
  return Array.isArray(value);
183
189
  },
184
190
 
191
+ isString(value: boolean | number | string): value is boolean {
192
+ return typeof value === 'string';
193
+ },
194
+
185
195
  /**
186
196
  * Finds the first true value for multiple checkboxes.
187
197
  * @param value A collection of values for the checkbox.
@@ -4,6 +4,20 @@ SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
4
4
  BASE_DIR="$( cd $SCRIPT_DIR && cd ../.. & pwd)"
5
5
  SHELL_DIR=$BASE_DIR/shell/
6
6
  EXIT_CODE=0
7
+ FORMATS="umd-min"
8
+
9
+ while getopts "df:" opt; do
10
+ case $opt in
11
+ d)
12
+ FORMATS="umd"
13
+ ;;
14
+ f)
15
+ FORMATS=$OPTARG
16
+ ;;
17
+ esac
18
+ done
19
+
20
+ shift $((OPTIND-1))
7
21
 
8
22
  # Use shell folder in node modules when we have @rancher/shell installed as a node module
9
23
  # rather than the use-case of the mono-repo with the shell folder at the top-level
@@ -12,7 +26,12 @@ if [ ! -d ${SHELL_DIR} ]; then
12
26
  SHELL_DIR=$(cd -P ${SHELL_DIR} && pwd)
13
27
  fi
14
28
 
15
- VERSION=$(cd pkg/$1; node -p -e "require('./package.json').version")
29
+ CREATE_TARBALL=${2}
30
+
31
+ if [ -z "$VERSION" ]; then
32
+ VERSION=$(cd pkg/$1; node -p -e "require('./package.json').version")
33
+ fi
34
+
16
35
  NAME=${1}-${VERSION}
17
36
  PKG_DIST=${BASE_DIR}/dist-pkg/${NAME}
18
37
 
@@ -20,6 +39,7 @@ if [ -d "${BASE_DIR}/pkg/${1}" ]; then
20
39
  echo "Building UI Package $1"
21
40
  echo " Package name: ${NAME}"
22
41
  echo " Package version: ${VERSION}"
42
+ echo " Output formats: ${FORMATS}"
23
43
  rm -rf ${PKG_DIST}
24
44
  mkdir -p ${PKG_DIST}
25
45
 
@@ -42,7 +62,11 @@ if [ -d "${BASE_DIR}/pkg/${1}" ]; then
42
62
  FILE=index.ts
43
63
  fi
44
64
 
45
- ${BASE_DIR}/node_modules/.bin/vue-cli-service build --name ${NAME} --target lib ${FILE} --dest ${PKG_DIST} --formats umd-min --filename ${NAME}
65
+ if [ -n "$COMMIT" ]; then
66
+ echo ${COMMIT} > ${PKG_DIST}/version
67
+ fi
68
+
69
+ ${BASE_DIR}/node_modules/.bin/vue-cli-service build --name ${NAME} --target lib ${FILE} --dest ${PKG_DIST} --formats ${FORMATS} --filename ${NAME}
46
70
  EXIT_CODE=$?
47
71
  cp -f ./package.json ${PKG_DIST}/package.json
48
72
  node ${SCRIPT_DIR}/pkgfile.js ${PKG_DIST}/package.json
@@ -52,4 +76,26 @@ if [ -d "${BASE_DIR}/pkg/${1}" ]; then
52
76
  popd
53
77
  fi
54
78
 
79
+ if [ $EXIT_CODE -ne 0 ]; then
80
+ exit $EXIT_CODE
81
+ fi
82
+
83
+
84
+ if [ -n "${CREATE_TARBALL}" ]; then
85
+ echo $COMMIT $COMMIT_BRANCH > ${PKG_DIST}/version-commit.txt
86
+
87
+ TARBALL=${NAME}.tar.gz
88
+
89
+ pushd ${PKG_DIST}
90
+
91
+ rm -f ../$TARBALL
92
+
93
+ echo "Compressing to ${TARBALL}..."
94
+
95
+ tar -czf ../${TARBALL} .
96
+
97
+ popd
98
+
99
+ fi
100
+
55
101
  exit $EXIT_CODE
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bash
2
+
3
+ yarn --pure-lockfile install
4
+
5
+ source scripts/version
6
+
7
+ if [[ $COMMIT_BRANCH == "master" ]]; then
8
+ VERSION="latest"
9
+ else
10
+ VERSION=$(cd pkg/$1; node -p -e "require('./package.json').version")
11
+ fi
12
+
13
+ echo "Drone Build Args"
14
+ echo "COMMIT: ${COMMIT}"
15
+ echo "COMMIT_BRANCH: ${COMMIT_BRANCH}"
16
+ echo "VERSION: ${VERSION}"
17
+ echo ""
18
+
19
+ # package, override version, commit for file in pkg root
20
+ # Note - in the future env vars should be moved to args and build-pkg should use getopts
21
+ COMMIT=$COMMIT COMMIT_BRANCH=$COMMIT_BRANCH VERSION=$VERSION ./shell/scripts/build-pkg.sh ${1} "true"
22
+ EXIT_CODE=$?
23
+
24
+ export PKG_NAME=${1}-${VERSION}
25
+ export PKG_TARBALL=${PKG_NAME}.tar.gz
26
+
27
+ echo "Drone Build Artefacts"
28
+ echo "Package Directory: ${PKG_NAME}"
29
+ echo "Package Tarball: ${PKG_TARBALL}"
30
+
31
+ exit $EXIT_CODE
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env bash
2
2
 
3
- SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
4
- BASE_DIR="$( cd $SCRIPT_DIR && cd ../.. & pwd)"
3
+ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
4
+ BASE_DIR="$(
5
+ cd $SCRIPT_DIR && cd ../.. &
6
+ pwd
7
+ )"
5
8
  SHELL_DIR=$BASE_DIR/shell/
6
9
  PUBLISH_ARGS="--no-git-tag-version --access public"
7
10
 
@@ -18,10 +21,10 @@ mkdir -p ${PKG_DIST}
18
21
  rm -rf ${PKG_DIST}/app
19
22
  rm -rf ${PKG_DIST}/pkg
20
23
 
21
- pushd ${SHELL_DIR} > /dev/null
24
+ pushd ${SHELL_DIR} >/dev/null
22
25
 
23
- PKG_VERSION=`node -p "require('./package.json').version"`
24
- popd > /dev/null
26
+ PKG_VERSION=$(node -p "require('./package.json').version")
27
+ popd >/dev/null
25
28
 
26
29
  echo "Publishing version: $PKG_VERSION"
27
30
 
@@ -39,7 +42,7 @@ function publish() {
39
42
  FOLDER=$2
40
43
 
41
44
  echo "Publishing ${NAME} from ${FOLDER}"
42
- pushd ${FOLDER} > /dev/null
45
+ pushd ${FOLDER} >/dev/null
43
46
 
44
47
  # For now, copy the rancher components into the shell and ship them with it
45
48
  if [ "$NAME" == "Shell" ]; then
@@ -50,11 +53,7 @@ function publish() {
50
53
 
51
54
  yarn publish . --new-version ${PKG_VERSION} ${PUBLISH_ARGS}
52
55
  RET=$?
53
-
54
- # Remove the rancher-components folder if we created it
55
- rm -rf ${SHELL_DIR}/rancher-components
56
-
57
- popd > /dev/null
56
+ popd >/dev/null
58
57
 
59
58
  if [ $RET -ne 0 ]; then
60
59
  echo "Error publishing package ${NAME}"
@@ -9,13 +9,14 @@ const dir = path.resolve('.');
9
9
  const pkgs = path.join(dir, 'dist-pkg');
10
10
  let port = 4500;
11
11
 
12
- var express = require('express')
13
- var serveStatic = require('serve-static');
14
-
15
- var app = express()
12
+ const express = require('express');
13
+ const serveStatic = require('serve-static');
14
+
15
+ const app = express();
16
16
 
17
17
  function catalog(res) {
18
18
  const response = [];
19
+
19
20
  fs.readdirSync(pkgs).forEach((f) => {
20
21
  const pkgFile = path.join(pkgs, f, 'package.json');
21
22
 
@@ -27,7 +28,7 @@ function catalog(res) {
27
28
  }
28
29
  });
29
30
 
30
- res.json(response)
31
+ res.json(response);
31
32
  }
32
33
 
33
34
  app.use('/', (req, res, next) => {
@@ -37,7 +38,7 @@ app.use('/', (req, res, next) => {
37
38
 
38
39
  return next();
39
40
  });
40
-
41
+
41
42
  app.use(serveStatic(pkgs));
42
43
 
43
44
  if (process.env.PORT) {
@@ -47,16 +48,22 @@ if (process.env.PORT) {
47
48
  const base = `http://127.0.0.1:${ port }`;
48
49
 
49
50
  console.log('');
50
- console.log(`Serving catalog on ${ base }`)
51
+ console.log(`Serving catalog on ${ base }`);
51
52
  console.log('');
52
53
  console.log(`Serving packages:`);
53
54
  console.log('');
54
55
  fs.readdirSync(pkgs).forEach((f) => {
55
- const main = `${ f }.umd.min.js`;
56
+ let main = `${ f }.umd.min.js`;
56
57
 
57
58
  if (fs.existsSync(path.join(pkgs, f, main))) {
58
59
  console.log(` ${ f } available at: ${ base }/${ f }/${ main }`);
60
+ } else {
61
+ main = `${ f }.umd.js`;
62
+
63
+ if (fs.existsSync(path.join(pkgs, f, main))) {
64
+ console.log(` ${ f } available at: ${ base }/${ f }/${ main }`);
65
+ }
59
66
  }
60
67
  });
61
-
62
- app.listen(port)
68
+
69
+ app.listen(port);