From 30e9e4c938d4f6c0df4477e22487091d64b53240 Mon Sep 17 00:00:00 2001 From: Matt Jones Date: Wed, 17 Apr 2024 19:56:54 -0800 Subject: [PATCH 001/169] Initial helm chart for debugging purposes only. --- helm/.helmignore | 23 +++++ helm/Chart.yaml | 24 +++++ helm/README.md | 33 +++++++ helm/metacatui-pv.yaml | 14 +++ helm/metacatui-pvc.yaml | 14 +++ helm/templates/.ingress.yaml.swp | Bin 0 -> 12288 bytes helm/templates/NOTES.txt | 22 +++++ helm/templates/_helpers.tpl | 62 +++++++++++++ helm/templates/deployment.yaml | 72 +++++++++++++++ helm/templates/hpa.yaml | 32 +++++++ helm/templates/ingress.yaml | 61 +++++++++++++ helm/templates/service.yaml | 15 ++++ helm/templates/serviceaccount.yaml | 13 +++ helm/templates/tests/test-connection.yaml | 15 ++++ helm/values.yaml | 104 ++++++++++++++++++++++ 15 files changed, 504 insertions(+) create mode 100644 helm/.helmignore create mode 100644 helm/Chart.yaml create mode 100644 helm/README.md create mode 100644 helm/metacatui-pv.yaml create mode 100644 helm/metacatui-pvc.yaml create mode 100644 helm/templates/.ingress.yaml.swp create mode 100644 helm/templates/NOTES.txt create mode 100644 helm/templates/_helpers.tpl create mode 100644 helm/templates/deployment.yaml create mode 100644 helm/templates/hpa.yaml create mode 100644 helm/templates/ingress.yaml create mode 100644 helm/templates/service.yaml create mode 100644 helm/templates/serviceaccount.yaml create mode 100644 helm/templates/tests/test-connection.yaml create mode 100644 helm/values.yaml diff --git a/helm/.helmignore b/helm/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/helm/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/Chart.yaml b/helm/Chart.yaml new file mode 100644 index 000000000..585d738d0 --- /dev/null +++ b/helm/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: metacatui +description: MetacatUI, a web interface for DataONE repositories + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.4.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "latest" diff --git a/helm/README.md b/helm/README.md new file mode 100644 index 000000000..5052d0fd1 --- /dev/null +++ b/helm/README.md @@ -0,0 +1,33 @@ +# MetacatUI Helm chart + +This is a simple helm chart for debugging a MetacatUI deployment. It works by: + +0. Create a namespace `mcui` for the deployment (or pick another of your liking) +1. Create a PV that is mapped to a local `hostPath` directory that contains the web files to deploy +2. Create a PVC for the PV +3. Create a nginx deployment behind an ingress that mounts the PVC where nginx expects its web files to live + +To deploy this, you need to 1) create the PV and PVC for your system layout, 2) modify the values.yaml to your hostname for the Ingress definition, and 3) install the helm chart: + +```bash +❯ helm -n mcui upgrade --install mcui ./helm +Release "mcui" has been upgraded. Happy Helming! +NAME: mcui +LAST DEPLOYED: Wed Apr 17 19:45:58 2024 +NAMESPACE: mcui +STATUS: deployed +REVISION: 11 +NOTES: +1. Get the application URL by running these commands: + http://firn.local/ +``` + +You can now edit the MetacatUI files and changes will be immediately visible in your chart. You will likely need to edit the `config.js` file to get a minimal setup working. For example, I used the `config/config.js` with these contents: + +```javascript +MetacatUI.AppConfig = { + root: "/", + baseUrl: "https://dev.nceas.ucsb.edu/knb/d1/mn" +} +``` + diff --git a/helm/metacatui-pv.yaml b/helm/metacatui-pv.yaml new file mode 100644 index 000000000..689934a4a --- /dev/null +++ b/helm/metacatui-pv.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: metacatui-pv +spec: + accessModes: + - ReadWriteOnce + capacity: + storage: 10Gi + hostPath: + path: /Users/jones/development/metacatui/src + type: "" + persistentVolumeReclaimPolicy: Retain + volumeMode: Filesystem diff --git a/helm/metacatui-pvc.yaml b/helm/metacatui-pvc.yaml new file mode 100644 index 000000000..a478b54b1 --- /dev/null +++ b/helm/metacatui-pvc.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: metacatui-pvc + namespace: mcui +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + storageClassName: "" + volumeMode: Filesystem + volumeName: metacatui-pv diff --git a/helm/templates/.ingress.yaml.swp b/helm/templates/.ingress.yaml.swp new file mode 100644 index 0000000000000000000000000000000000000000..c3a32c5bf7a659eb55c245209d05148a56e3dace GIT binary patch literal 12288 zcmeI2&yUt6r>_g#fD|AFNC8rS6d(mif&YO5HeF#ikmqA%uIuG_rExBsbRq>v0aAbzAO%PP zQh*d71xNu>fD|AFNP$OC0Wo0g*C!aecL2fT|NqtB|M#9_>|1aX909*P%h*Nm7I*{v zbBM7&z?UEdo8UEY0vrV|fd39M_BZ$$+yS?M1RsL;!2M?!`wn~qZiAcPV{ieS1TTWe z!Cy}^_8Yhhegt2G97NzF@H#jOzJH3bufSEX1rCFMpJeQJ@DsQLz5wrlSHMB=0D0dB zF8@EB26T`DqyQ;E3XlSix&l|Ow7Jv+e(f6LI2uiNYtGGu9Y$v3D3tFeneAarzG}vy zYgBIhu>PmCydYv(SPpZgTx;6Y$EM7T9ygNCy-oEu~$?CAZSw2&{TTFnv5Fa@T1*XG!)s825)_3^z<+CCIN1K!2nUKc{hUS8YP`fKQd>( zIptRFGLx{BPaokNPkv`x+EY!G#>>R@tbW3K>sWnu;Y_x+v2kY5#;x*X^6j@TU?e@S zTK#LyPydjkeE6}EFz4mY)C2XRS?J2wZ#m#T>%{w;yTdM{-tmdI$qN)xRmqkwq` l{ET`vC+|;KM$=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "metacatui.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml new file mode 100644 index 000000000..785db5717 --- /dev/null +++ b/helm/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "metacatui.fullname" . }} + labels: + {{- include "metacatui.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "metacatui.selectorLabels" . | nindent 4 }} diff --git a/helm/templates/serviceaccount.yaml b/helm/templates/serviceaccount.yaml new file mode 100644 index 000000000..84f602059 --- /dev/null +++ b/helm/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "metacatui.serviceAccountName" . }} + labels: + {{- include "metacatui.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm/templates/tests/test-connection.yaml b/helm/templates/tests/test-connection.yaml new file mode 100644 index 000000000..98a51f6dc --- /dev/null +++ b/helm/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "metacatui.fullname" . }}-test-connection" + labels: + {{- include "metacatui.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "metacatui.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/values.yaml b/helm/values.yaml new file mode 100644 index 000000000..d64486960 --- /dev/null +++ b/helm/values.yaml @@ -0,0 +1,104 @@ +# Default values for metacatui. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "latest" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: false + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: true + className: traefik + annotations: + #traefik.ingress.kubernetes.io/router.entrypoints: web + kubernetes.io/ingress.class: traefik + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: firn.local + paths: + - path: / + pathType: Prefix + backend: + service: + name: mcui-metacatui + port: + number: 80 + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: + - name: metacatui-pvc + persistentVolumeClaim: + claimName: metacatui-pvc + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: + - name: metacatui-pvc + mountPath: "/usr/share/nginx/html" + readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} From d2e833b2bd4c24eef1cab661f2b7bee155e62025 Mon Sep 17 00:00:00 2001 From: Matt Jones Date: Wed, 17 Apr 2024 20:58:58 -0800 Subject: [PATCH 002/169] Disable Ingress by default to avoid accidental deploys. --- helm/values.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helm/values.yaml b/helm/values.yaml index d64486960..47ad6fce8 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -44,7 +44,7 @@ service: port: 80 ingress: - enabled: true + enabled: false className: traefik annotations: #traefik.ingress.kubernetes.io/router.entrypoints: web @@ -58,7 +58,7 @@ ingress: pathType: Prefix backend: service: - name: mcui-metacatui + name: mcui-metacatui # Assumes the service is exposed in the mcui namespace port: number: 80 tls: [] From 28f685954bff54cdb74ef74654034ba95b3430a5 Mon Sep 17 00:00:00 2001 From: Rushiraj Nenuji Date: Wed, 1 May 2024 11:12:40 -0700 Subject: [PATCH 003/169] Only use inline import, remove the top level model import to avoid circular dependency Only use inline import, remove the top level model import to avoid circular dependency Reference: #2395 --- src/js/collections/Citations.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/js/collections/Citations.js b/src/js/collections/Citations.js index bafe747dc..66f6f4f33 100644 --- a/src/js/collections/Citations.js +++ b/src/js/collections/Citations.js @@ -1,8 +1,8 @@ /* global define */ "use strict"; -define(['jquery', 'underscore', 'backbone', 'models/CitationModel'], - function($, _, Backbone, CitationModel) { +define(['jquery', 'underscore', 'backbone'], + function($, _, Backbone) { /** * @class Citations @@ -18,7 +18,7 @@ define(['jquery', 'underscore', 'backbone', 'models/CitationModel'], /** @lends Citations.prototype */{ model: function (attrs, options) { - // We use the inline require here in addition to the define above to + // We use the inline require here to // avoid an issue caused by the circular dependency between // CitationModel and Citations var CitationModel = require('models/CitationModel'); From e2e9b7c862200ff3993ddf5f0372641f7d8b3463 Mon Sep 17 00:00:00 2001 From: Rushiraj Nenuji Date: Thu, 2 May 2024 13:00:38 -0700 Subject: [PATCH 004/169] Restore dual require for Citations collection; add dual require for Citation Model Restore dual require for Citations collection; add dual require for Citation Model #2395 --- src/js/collections/Citations.js | 6 +++--- src/js/models/CitationModel.js | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/js/collections/Citations.js b/src/js/collections/Citations.js index 66f6f4f33..bafe747dc 100644 --- a/src/js/collections/Citations.js +++ b/src/js/collections/Citations.js @@ -1,8 +1,8 @@ /* global define */ "use strict"; -define(['jquery', 'underscore', 'backbone'], - function($, _, Backbone) { +define(['jquery', 'underscore', 'backbone', 'models/CitationModel'], + function($, _, Backbone, CitationModel) { /** * @class Citations @@ -18,7 +18,7 @@ define(['jquery', 'underscore', 'backbone'], /** @lends Citations.prototype */{ model: function (attrs, options) { - // We use the inline require here to + // We use the inline require here in addition to the define above to // avoid an issue caused by the circular dependency between // CitationModel and Citations var CitationModel = require('models/CitationModel'); diff --git a/src/js/models/CitationModel.js b/src/js/models/CitationModel.js index 817f44cc4..c839f946d 100644 --- a/src/js/models/CitationModel.js +++ b/src/js/models/CitationModel.js @@ -156,6 +156,11 @@ define(["jquery", "underscore", "backbone", "collections/Citations"], function ( // Format the citation metadata = DataONE datasets cited by this // citation (external document) const cm = response.citationMetadata; + + // We use the inline require here in addition to the define above to + // avoid an issue caused by the circular dependency between + // CitationModel and Citations + var Citations = require('collections/Citations'); if (cm) { if (cm && !(cm instanceof Citations)) { const citationMetadata = Object.entries(cm).map(([pid, data]) => { From 07b12434eb2cb1a52b2dfbff2f53ffaad27ee393 Mon Sep 17 00:00:00 2001 From: Rushiraj Nenuji Date: Mon, 3 Jun 2024 08:44:59 -0700 Subject: [PATCH 005/169] Use the proper packageService download URL Use the proper packageService download URL Reference: #2373 #2424 --- src/js/views/DownloadButtonView.js | 3 +-- src/js/views/MetadataView.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/js/views/DownloadButtonView.js b/src/js/views/DownloadButtonView.js index 262c3dd1a..97151d1f7 100644 --- a/src/js/views/DownloadButtonView.js +++ b/src/js/views/DownloadButtonView.js @@ -35,8 +35,7 @@ define(['jquery', 'underscore', 'backbone', 'models/SolrResult', 'models/DataONE (this.model.get("type") == "DataPackage"))) { hrefLink = this.model.getPackageURL(); } - if (this.model instanceof PackageModel && - this.nested && + if (this.model instanceof PackageModel && ((this.model.get("formatType") == "RESOURCE") || (this.model.get("type") == "DataPackage") || (this.model.get("type") == "Package"))) { diff --git a/src/js/views/MetadataView.js b/src/js/views/MetadataView.js index 41af84291..54d1cea76 100644 --- a/src/js/views/MetadataView.js +++ b/src/js/views/MetadataView.js @@ -968,7 +968,7 @@ define(['jquery', // Add Package Download // create an instance of DownloadButtonView to handle package downloads - this.downloadButtonView = new DownloadButtonView({id: packageModel.get("id"), model: packageModel, view: "actionsView"}); + this.downloadButtonView = new DownloadButtonView({ model: packageModel, view: "actionsView"}); // render this.downloadButtonView.render(); From 753f37e47426bfcab864022956275bf05479c8c2 Mon Sep 17 00:00:00 2001 From: Rushiraj Nenuji Date: Sun, 26 May 2024 12:48:15 -0700 Subject: [PATCH 006/169] Decode entity PIDs from the view service properly Decode entity PIDs from the view service properly Reference: #2403 --- src/js/views/MetadataView.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/js/views/MetadataView.js b/src/js/views/MetadataView.js index 41af84291..701443927 100644 --- a/src/js/views/MetadataView.js +++ b/src/js/views/MetadataView.js @@ -3156,13 +3156,24 @@ define(['jquery', }, storeEntityPIDs: function(responseEl) { - var view = this; - _.each($(responseEl).find(".entitydetails"), function (entityEl) { - var entityId = $(entityEl).data("id"); - view.entities.push(entityId.replace('urn-uuid-', 'urn:uuid:')); - }); + var view = this; + _.each($(responseEl).find(".entitydetails"), function (entityEl) { + var entityId = $(entityEl).data("id"); + + // Check and replace urn-uuid- with urn:uuid: if the string starts with urn-uuid- + if (entityId.startsWith('urn-uuid-')) { + entityId = entityId.replace('urn-uuid-', 'urn:uuid:'); + } + + // Check and replace doi-10. with doi:10. if the string starts with doi-10. + if (entityId.startsWith('doi-10.')) { + entityId = entityId.replace('doi-10.', 'doi:10.'); + } + + view.entities.push(entityId); + }); } - }); + }); return MetadataView; }); From 300e68f28a0df068069f525d06b71a30a68f01b2 Mon Sep 17 00:00:00 2001 From: Rushiraj Nenuji Date: Mon, 3 Jun 2024 10:57:01 -0700 Subject: [PATCH 007/169] Use the DownloadButtonView instead of reconstructing the download URL Use the DownloadButtonView instead of reconstructing the download URL Reference: #2424 --- src/js/views/DataItemView.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/js/views/DataItemView.js b/src/js/views/DataItemView.js index 279deaca0..dfdcb3bcf 100644 --- a/src/js/views/DataItemView.js +++ b/src/js/views/DataItemView.js @@ -539,21 +539,6 @@ define([ } //Download button - attributes.downloadUrl = undefined; - if (this.model.get("dataUrl") !== undefined || - this.model.get("url") !== undefined || - this.model.url() !== undefined) { - if (this.model.get("dataUrl") !== undefined) { - attributes.downloadUrl = this.model.get("dataUrl"); - } - else if (this.model.get("url") !== undefined) { - attributes.downloadUrl = this.model.get("url"); - } - else if (this.model.url() !== undefined) { - var downloadUrl = this.model.url(); - attributes.downloadUrl = downloadUrl.replace("/meta/", "/object/"); - } - } this.downloadButtonView = new DownloadButtonView({ model: this.model, view: "actionsView" }); this.downloadButtonView.render(); From a17d85379345bc62533b8d836afeb7832d60a982 Mon Sep 17 00:00:00 2001 From: Rushiraj Nenuji Date: Mon, 3 Jun 2024 10:57:51 -0700 Subject: [PATCH 008/169] Update query selector to only update the package header URL Update query selector to only update the package header URL Reference: #2424 --- src/js/templates/dataPackageHeader.html | 2 +- src/js/views/DataPackageView.js | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/js/templates/dataPackageHeader.html b/src/js/templates/dataPackageHeader.html index 11426b4bc..0ae8a8a3f 100644 --- a/src/js/templates/dataPackageHeader.html +++ b/src/js/templates/dataPackageHeader.html @@ -35,7 +35,7 @@ <% if (!disablePackageDownloads) { %> - + <% } %> diff --git a/src/js/views/DataPackageView.js b/src/js/views/DataPackageView.js index 4b708dd2a..84496aa34 100644 --- a/src/js/views/DataPackageView.js +++ b/src/js/views/DataPackageView.js @@ -890,16 +890,12 @@ define([ let titleTooltip = title; title = (title.length > 150) ? title.slice(0, 75) + "..." + title.slice(title.length - 75, title.length) : title; - // Set the package URL - if (MetacatUI.appModel.get("packageServiceUrl")) - packageUrl = MetacatUI.appModel.get("packageServiceUrl") + encodeURIComponent(dataPackage.id); - /** * The HTML content for the data package header. * * @type {string} */ - tableRow = this.dataPackageHeaderTemplate({ id: dataPackage.id, title: title, titleTooltip: titleTooltip, disablePackageDownloads: false, downloadUrl: packageUrl }); + tableRow = this.dataPackageHeaderTemplate({ id: dataPackage.id, title: title, titleTooltip: titleTooltip, disablePackageDownloads: false }); this.$el.append(tableRow); // Create an instance of DownloadButtonView to handle package downloads @@ -909,7 +905,7 @@ define([ this.downloadButtonView.render(); // Add the downloadButtonView el to the span - this.$el.find('.downloadAction').html(this.downloadButtonView.el); + this.$el.find('.downloadAction[data-id="' + dataPackage.id + '"]').html(this.downloadButtonView.el); // Filter out the packages from the member list members = _.filter(members, function(m) { return (m.type != "Package") }); From 2acec17360c1626e8b4feece2ac9ed6feff35444 Mon Sep 17 00:00:00 2001 From: Rushiraj Nenuji Date: Mon, 3 Jun 2024 11:44:12 -0700 Subject: [PATCH 009/169] Add link to open nested package in actionsPanel Add link to open nested package in actionsPanel Reference: #2418 --- src/css/metacatui-common.css | 4 ++++ src/js/templates/dataPackageHeader.html | 10 ++++++++++ src/js/views/DataPackageView.js | 12 ++++++++++-- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/css/metacatui-common.css b/src/css/metacatui-common.css index 55b684fd7..4de873ab3 100644 --- a/src/css/metacatui-common.css +++ b/src/css/metacatui-common.css @@ -9189,6 +9189,10 @@ body > #extension-is-installed{ margin-left: -4px; } +.btn.btn-rounded.action.viewAction > i { + margin-left: -2px; +} + /* Data Package Item */ .data-package-item { height: 38px; diff --git a/src/js/templates/dataPackageHeader.html b/src/js/templates/dataPackageHeader.html index 0ae8a8a3f..c90cfb601 100644 --- a/src/js/templates/dataPackageHeader.html +++ b/src/js/templates/dataPackageHeader.html @@ -34,6 +34,16 @@ + + + <% if (!disablePackageUrl && packageUrl !== undefined && packageUrl) { %> + + + + + + <% } %> + <% if (!disablePackageDownloads) { %> <% } %> diff --git a/src/js/views/DataPackageView.js b/src/js/views/DataPackageView.js index 84496aa34..f1fa7e4e3 100644 --- a/src/js/views/DataPackageView.js +++ b/src/js/views/DataPackageView.js @@ -443,7 +443,7 @@ define([ packageUrl = MetacatUI.appModel.get("packageServiceUrl") + encodeURIComponent(view.dataPackage.id); var disablePackageDownloads = this.disablePackageDownloads; - tableRow = this.dataPackageHeaderTemplate({id:view.dataPackage.id, title: title, titleTooltip: titleTooltip, downloadUrl: packageUrl, disablePackageDownloads: disablePackageDownloads}); + tableRow = this.dataPackageHeaderTemplate({id:view.dataPackage.id, title: title, titleTooltip: titleTooltip, downloadUrl: packageUrl, disablePackageDownloads: disablePackageDownloads, disablePackageUrl: true}); this.$el.append(tableRow); @@ -890,12 +890,20 @@ define([ let titleTooltip = title; title = (title.length > 150) ? title.slice(0, 75) + "..." + title.slice(title.length - 75, title.length) : title; + + /** + * The View URL for this nested package. + * + * @type {string} + */ + let nestedPackageUrl = MetacatUI.root + "/view/" + dataPackage.id; + /** * The HTML content for the data package header. * * @type {string} */ - tableRow = this.dataPackageHeaderTemplate({ id: dataPackage.id, title: title, titleTooltip: titleTooltip, disablePackageDownloads: false }); + tableRow = this.dataPackageHeaderTemplate({ id: dataPackage.id, title: title, titleTooltip: titleTooltip, disablePackageDownloads: false, disablePackageUrl: false, packageUrl: nestedPackageUrl }); this.$el.append(tableRow); // Create an instance of DownloadButtonView to handle package downloads From edda8943614874f39d224a2ed2eb8817aba253b2 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 3 Jun 2024 15:25:08 -0700 Subject: [PATCH 010/169] add configmap for config.js; git checkout static content --- helm/config/config.js | 28 ++++++++++++ helm/templates/configmap.yaml | 9 ++++ helm/templates/deployment.yaml | 52 +++++++++++++++++----- helm/values.yaml | 80 +++++++++++++++++++++++++++++----- 4 files changed, 147 insertions(+), 22 deletions(-) create mode 100644 helm/config/config.js create mode 100644 helm/templates/configmap.yaml diff --git a/helm/config/config.js b/helm/config/config.js new file mode 100644 index 000000000..19ba4ed47 --- /dev/null +++ b/helm/config/config.js @@ -0,0 +1,28 @@ +MetacatUI.AppConfig = { + root: {{ .Values.appConfig.root | quote }}, + theme: {{ .Values.appConfig.theme | quote }}, + baseUrl: {{ .Values.appConfig.baseUrl | quote }} + {{- $optionalStringValues := list + "d1CNBaseUrl" + "mapKey" + "mdqBaseUrl" + "dataoneSearchUrl" + "googleAnalyticsKey" + "bioportalAPIKey" + "cesiumToken" + -}} + {{- $optionalIntValues := list + "portalLimit" + -}} + {{- range $key, $value := .Values.appConfig }} + {{- if has $key $optionalStringValues }} + {{- (printf ",") }} + {{- (printf "%s: \"%s\"" $key (toString $value)) | nindent 6 }} + {{- else }} + {{- if has $key $optionalIntValues }} + {{- (printf ",") }} + {{- (printf "%s: %s" $key (toString $value)) | nindent 6 }} + {{- end }} + {{- end }} + {{- end }} +} diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml new file mode 100644 index 000000000..55948e4d0 --- /dev/null +++ b/helm/templates/configmap.yaml @@ -0,0 +1,9 @@ +# Load all files in the "config" directory into a ConfigMap +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-metacatui-configjs + labels: + {{- include "metacatui.labels" . | nindent 4 }} +data: +{{ (tpl (.Files.Glob "config/*").AsConfig . ) | nindent 4 }} diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 3154d3292..8002a4c4f 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -30,6 +30,19 @@ spec: serviceAccountName: {{ include "metacatui.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- if .Values.git.enabled }} + initContainers: + - name: git-clone + image: alpine/git + command: + - sh + - -c + - > + git clone -b {{ .Values.git.revision }} --depth 1 {{ .Values.git.repoUrl }} /metacatui + volumeMounts: + - name: {{ .Release.Name }}-mcui-static-files + mountPath: /metacatui + {{- end }} containers: - name: {{ .Chart.Name }} securityContext: @@ -40,24 +53,43 @@ spec: - name: http containerPort: {{ .Values.service.port }} protocol: TCP + {{- with .Values.livenessProbe }} livenessProbe: - httpGet: - path: / - port: http + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} readinessProbe: - httpGet: - path: / - port: http + {{- toYaml . | nindent 12 }} + {{- end }} resources: {{- toYaml .Values.resources | nindent 12 }} - {{- with .Values.volumeMounts }} volumeMounts: + {{- if .Values.appConfig.enabled }} + - name: {{ .Release.Name }}-mcui-config-vol + mountPath: /usr/share/nginx/html/config/config.js + subPath: config.js + {{- end }} + - name: {{ .Release.Name }}-mcui-static-files + mountPath: "/usr/share/nginx/html" + subPath: "src" + {{- with .Values.volumeMounts }} {{- toYaml . | nindent 12 }} {{- end }} - {{- with .Values.volumes }} volumes: - {{- toYaml . | nindent 8 }} - {{- end }} + {{- if .Values.appConfig.enabled }} + - name: {{ .Release.Name }}-mcui-config-vol + configMap: + name: {{ .Release.Name }}-metacatui-configjs + defaultMode: 0644 + {{- end }} + {{- if .Values.git.enabled }} + - name: {{ .Release.Name }}-mcui-static-files + emptyDir: {} + {{- else }} + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/helm/values.yaml b/helm/values.yaml index 47ad6fce8..c005ab75c 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -4,6 +4,65 @@ replicaCount: 1 +## appConfig contains the MetacatUI.AppConfig settings that can be overridden. Uses a configMap to +## define MetacatUI.AppConfig, replacing the version of config/config.js found on the mounted drive. +## Note that changes to the configMap will not be read unless the pod is restarted, and changes to +## these values will not be reflected in the configMap unless you do a 'helm upgrade'. +## +appConfig: + ## appConfig.enabled Use a configMap to define MetacatUI.AppConfig, replacing the on-disk version + ## Typical use is "false" for development purposes, and "true" for more-permanent deployments + ## + enabled: true + + ## appConfig.root (required) The url root to be appended after the appConfig.baseUrl, below + ## + root: "/" + + ## appConfig.theme (required) Corresponds to the name of one of the directories in src/js/themes/ + ## + theme: "default" + + ## appConfig.baseUrl (required) the base url (typically defined by the ingress) + ## + baseUrl: "http://localhost:8080/" + + ## Optional configuration -- leave commented to use metacatui default values + ## +# d1CNBaseUrl: "https://cn-sandbox.test.dataone.org/cn" +# mapKey: "your-map-key-here" +# mdqBaseUrl: "https://localhost:8080:30443/quality" +# dataoneSearchUrl: "https://search-stage.test.dataone.org" +# portalLimit: 100 +# googleAnalyticsKey: "your-google-analytics-key-here" +# bioportalAPIKey: "your-bio-portal-api-key-here" +# cesiumToken: "your-cesium-token-here" + + +git: + ## git.enabled set 'true' to create an initContainer and do a git checkout of the metacatui files + ## NOTE: If you set git.enabled: 'false', then you will need to provide values for 'volumes' + ## that + enabled: true + + ## git.repoUrl the https url of the repo to be cloned + repoUrl: "https://github.com/NCEAS/metacatui.git" + + ## git.revision can be any string that makes sense after the command `git checkout`... - for + ## example: + ## git checkout tags/2.29.0 => revision: "tags/2.29.0" + ## git checkout develop => revision: "develop" + ## + revision: "main" + +## volumes Uncomment and provide values if you want to use a pre-configured PVC instead of doing a +## git checkout +#volumes: +# ## volumes.name substitute your own release name, but do NOT change the '-mcui-static-files' part +# - name: -mcui-static-files +# persistentVolumeClaim: +# claimName: + image: repository: nginx pullPolicy: IfNotPresent @@ -14,6 +73,15 @@ imagePullSecrets: [] nameOverride: "" fullnameOverride: "" +#livenessProbe: +# httpGet: +# path: / +# port: http +#readinessProbe: +# httpGet: +# path: / +# port: http + serviceAccount: # Specifies whether a service account should be created create: false @@ -85,18 +153,6 @@ autoscaling: targetCPUUtilizationPercentage: 80 # targetMemoryUtilizationPercentage: 80 -# Additional volumes on the output Deployment definition. -volumes: - - name: metacatui-pvc - persistentVolumeClaim: - claimName: metacatui-pvc - -# Additional volumeMounts on the output Deployment definition. -volumeMounts: - - name: metacatui-pvc - mountPath: "/usr/share/nginx/html" - readOnly: true - nodeSelector: {} tolerations: [] From 375646292f79dcfebb862ab2336532d527cbf3b7 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 3 Jun 2024 15:41:35 -0700 Subject: [PATCH 011/169] doc update --- helm/README.md | 21 +++++++++++++++++---- helm/config/config.js | 1 + 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/helm/README.md b/helm/README.md index 5052d0fd1..caacac8eb 100644 --- a/helm/README.md +++ b/helm/README.md @@ -1,6 +1,20 @@ # MetacatUI Helm chart -This is a simple helm chart for debugging a MetacatUI deployment. It works by: +This is a simple helm chart for debugging a MetacatUI deployment. + +## Steps to get started for deployment in a Kubernetes cluster: + +1. modify values.yaml as appropriate +2. install the helm chart: +```shell +$ helm -n knb upgrade --install knbmcui ./helm +``` +There's no need to set up any persistent storage - the chart will automatically check out the +metacatui static content from GitHub, and install it on an "emptyDir" that is automatically +created for you. + + +## Steps to get started for development on localhost (e.g. Rancher Desktop/Docker Desktop): 0. Create a namespace `mcui` for the deployment (or pick another of your liking) 1. Create a PV that is mapped to a local `hostPath` directory that contains the web files to deploy @@ -9,8 +23,8 @@ This is a simple helm chart for debugging a MetacatUI deployment. It works by: To deploy this, you need to 1) create the PV and PVC for your system layout, 2) modify the values.yaml to your hostname for the Ingress definition, and 3) install the helm chart: -```bash -❯ helm -n mcui upgrade --install mcui ./helm +```shell +$ helm -n mcui upgrade --install --debug mcui ./helm Release "mcui" has been upgraded. Happy Helming! NAME: mcui LAST DEPLOYED: Wed Apr 17 19:45:58 2024 @@ -30,4 +44,3 @@ MetacatUI.AppConfig = { baseUrl: "https://dev.nceas.ucsb.edu/knb/d1/mn" } ``` - diff --git a/helm/config/config.js b/helm/config/config.js index 19ba4ed47..0dbaa72af 100644 --- a/helm/config/config.js +++ b/helm/config/config.js @@ -2,6 +2,7 @@ MetacatUI.AppConfig = { root: {{ .Values.appConfig.root | quote }}, theme: {{ .Values.appConfig.theme | quote }}, baseUrl: {{ .Values.appConfig.baseUrl | quote }} + {{/* Add any new keys to these lists, and they will be populated automatically if set */}} {{- $optionalStringValues := list "d1CNBaseUrl" "mapKey" From ea093a17726742d6fc91a0f28c21aef7c44cea65 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Wed, 5 Jun 2024 09:54:03 -0700 Subject: [PATCH 012/169] move pv/pvc yaml and add notes --- helm/admin/metacatui-pv.yaml | 18 ++++++++++++++++++ helm/admin/metacatui-pvc.yaml | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 helm/admin/metacatui-pv.yaml create mode 100644 helm/admin/metacatui-pvc.yaml diff --git a/helm/admin/metacatui-pv.yaml b/helm/admin/metacatui-pv.yaml new file mode 100644 index 000000000..140e47a57 --- /dev/null +++ b/helm/admin/metacatui-pv.yaml @@ -0,0 +1,18 @@ +## EXAMPLE file for manually creating a Persistent Volume to store the metacatui source code, +## typically for development purposes (so edits can be seen in realtime). +## Needed only if 'source.from:' is set to 'pvc' in values.yaml. +## +apiVersion: v1 +kind: PersistentVolume +metadata: + name: metacatui-pv +spec: + accessModes: + - ReadWriteOnce + capacity: + storage: 10Gi + hostPath: + path: /your/host/path/here # e.g. /Users/jones/development/metacatui/src + type: "" + persistentVolumeReclaimPolicy: Retain + volumeMode: Filesystem diff --git a/helm/admin/metacatui-pvc.yaml b/helm/admin/metacatui-pvc.yaml new file mode 100644 index 000000000..d0e9fa491 --- /dev/null +++ b/helm/admin/metacatui-pvc.yaml @@ -0,0 +1,18 @@ +## EXAMPLE file for manually creating a Persistent Volume Claim to access the PV containing the +## metacatui source code (see metacatui-pv.yaml), typically for development purposes. +## Needed only if 'source.from:' is set to 'pvc' in values.yaml. +## +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: metacatui-pvc + namespace: mcui +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + storageClassName: "" + volumeMode: Filesystem + volumeName: metacatui-pv From 11bc6b5d6922fca49f0299f1af6482b748cc7e53 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Wed, 5 Jun 2024 09:54:50 -0700 Subject: [PATCH 013/169] final config.js --- helm/config/config.js | 40 +++++++++++++--------------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/helm/config/config.js b/helm/config/config.js index 0dbaa72af..dc05ae99d 100644 --- a/helm/config/config.js +++ b/helm/config/config.js @@ -1,29 +1,15 @@ MetacatUI.AppConfig = { - root: {{ .Values.appConfig.root | quote }}, - theme: {{ .Values.appConfig.theme | quote }}, - baseUrl: {{ .Values.appConfig.baseUrl | quote }} - {{/* Add any new keys to these lists, and they will be populated automatically if set */}} - {{- $optionalStringValues := list - "d1CNBaseUrl" - "mapKey" - "mdqBaseUrl" - "dataoneSearchUrl" - "googleAnalyticsKey" - "bioportalAPIKey" - "cesiumToken" - -}} - {{- $optionalIntValues := list - "portalLimit" - -}} - {{- range $key, $value := .Values.appConfig }} - {{- if has $key $optionalStringValues }} - {{- (printf ",") }} - {{- (printf "%s: \"%s\"" $key (toString $value)) | nindent 6 }} - {{- else }} - {{- if has $key $optionalIntValues }} - {{- (printf ",") }} - {{- (printf "%s: %s" $key (toString $value)) | nindent 6 }} - {{- end }} - {{- end }} - {{- end }} + {{- $ignoreList := list "enabled" "root" "baseUrl" -}} + {{- range $key, $value := .Values.appConfig }} + {{- if not (has $key $ignoreList) }} + {{- if eq (typeOf $value) "string" }} + {{- $key | nindent 4 }}: {{ $value | quote }}, + {{- else }} + {{- $key | nindent 4 }}: {{ $value }}, + {{- end }} + {{- end }} + {{- end -}} + {{/* These go last, so we can handle the trailing comma */}} + root: {{ required "root is REQUIRED" .Values.appConfig.root | quote }}, + baseUrl: {{ required "baseUrl is REQUIRED" .Values.appConfig.baseUrl | quote }} } From efa461d9137a7bd91e1b5160cb4b7f357fe3a7f2 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Wed, 5 Jun 2024 09:55:46 -0700 Subject: [PATCH 014/169] get source from official release zip, git, or PVC --- helm/templates/deployment.yaml | 33 ++++++++-- helm/values.yaml | 116 +++++++++++++++++++-------------- 2 files changed, 92 insertions(+), 57 deletions(-) diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 8002a4c4f..4e4a141d3 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -30,17 +30,36 @@ spec: serviceAccountName: {{ include "metacatui.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} - {{- if .Values.git.enabled }} + {{- if not (eq .Values.source.from "pvc") }} initContainers: + {{- if (eq .Values.source.from "git") }} - name: git-clone - image: alpine/git + image: alpine/git:latest command: - sh - -c - > - git clone -b {{ .Values.git.revision }} --depth 1 {{ .Values.git.repoUrl }} /metacatui + start=$(date +%s); + git clone -b {{ .Values.source.git.revision }} --depth 1 {{ .Values.source.git.repoUrl }} /metacatui; + finish=$(date +%s); + echo "git clone -b {{ .Values.source.git.revision }} took $((finish - start)) sec"; + {{- else }} + - name: get-source + image: busybox:latest + command: + - sh + - -c + - > + start=$(date +%s); + FILENAME={{ .Values.source.package.version }}.zip; + wget -O ./$FILENAME {{ .Values.source.package.location }}/$FILENAME; + unzip $FILENAME -d /tmp/; + mv /tmp/metacatui-{{ .Values.source.package.version }}/* /metacatui/; + finish=$(date +%s); + echo "$FILENAME download and install took $((finish - start)) sec"; + {{- end }} volumeMounts: - - name: {{ .Release.Name }}-mcui-static-files + - name: {{ .Release.Name }}-mcui-source-files mountPath: /metacatui {{- end }} containers: @@ -69,7 +88,7 @@ spec: mountPath: /usr/share/nginx/html/config/config.js subPath: config.js {{- end }} - - name: {{ .Release.Name }}-mcui-static-files + - name: {{ .Release.Name }}-mcui-source-files mountPath: "/usr/share/nginx/html" subPath: "src" {{- with .Values.volumeMounts }} @@ -82,8 +101,8 @@ spec: name: {{ .Release.Name }}-metacatui-configjs defaultMode: 0644 {{- end }} - {{- if .Values.git.enabled }} - - name: {{ .Release.Name }}-mcui-static-files + {{- if not .Values.source.pvc }} + - name: {{ .Release.Name }}-mcui-source-files emptyDir: {} {{- else }} {{- with .Values.volumes }} diff --git a/helm/values.yaml b/helm/values.yaml index c005ab75c..1891947d8 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -2,10 +2,49 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. -replicaCount: 1 +source: + ## source.from The source for the metacatui source code. Options are "package", "git", or "pvc" + ## * "package" will download a release package from the metacatui git repository, unzip it, and + ## install the files in local pod storage (emptyDir{}) + ## * "git" will clone a specific branch or tag from the metacatui git repository, and install the + ## files in local pod storage (emptyDir{}) + ## * "pvc" expects to find a pre-configured PVC containing the files to be used. Note you will + ## need to provide values for a pre-configured PVC in 'volumes', below + ## + from: package + + ## git settings ignored unless 'source.from:' is set to 'git' + git: + ## source.git.repoUrl the https url of the repo to be cloned + repoUrl: "https://github.com/NCEAS/metacatui.git" + + ## source.git.revision can be any string that makes sense after the command `git checkout`... + ## - for example: + ## git checkout tags/2.29.0 => revision: "tags/2.29.0" + ## git checkout develop => revision: "develop" + ## + revision: "develop" + + package: + ## source.package.location The remote location where the release zipfile is hosted + ## + location: "https://github.com/NCEAS/metacatui/archive" + + ## source.package.version The release version. Assumes release is a zipfile named .zip + ## example: + ## location: "https://github.com/NCEAS/metacatui/archive" + ## version: "2.29.1" + ## will download: https://github.com/NCEAS/metacatui/archive/2.29.1.zip + ## + version: "2.29.1" ## appConfig contains the MetacatUI.AppConfig settings that can be overridden. Uses a configMap to ## define MetacatUI.AppConfig, replacing the version of config/config.js found on the mounted drive. +## +## You can define any attributes here, that will be automatically inserted into config/config.js. +## For a full list of attributes that can be overridden, see AppModel.js: +## https://github.com/NCEAS/metacatui/blob/main/src/js/models/AppModel.js +## ## Note that changes to the configMap will not be read unless the pod is restarted, and changes to ## these values will not be reflected in the configMap unless you do a 'helm upgrade'. ## @@ -15,51 +54,30 @@ appConfig: ## enabled: true - ## appConfig.root (required) The url root to be appended after the appConfig.baseUrl, below + ## appConfig.root (required) The url root to be appended after the appConfig.baseUrl, below. ## root: "/" - ## appConfig.theme (required) Corresponds to the name of one of the directories in src/js/themes/ - ## - theme: "default" - - ## appConfig.baseUrl (required) the base url (typically defined by the ingress) + ## appConfig.baseUrl (required) the base url. (Typically defined by the ingress; used to contact + ## the metacat API.) ## baseUrl: "http://localhost:8080/" - ## Optional configuration -- leave commented to use metacatui default values + ## Optional configuration. Note you can define any attributes here, to override those that would + ## normally appear in config.js. See full listing in AppModel.js: + ## https://github.com/NCEAS/metacatui/blob/main/src/js/models/AppModel.js ## -# d1CNBaseUrl: "https://cn-sandbox.test.dataone.org/cn" -# mapKey: "your-map-key-here" -# mdqBaseUrl: "https://localhost:8080:30443/quality" -# dataoneSearchUrl: "https://search-stage.test.dataone.org" -# portalLimit: 100 -# googleAnalyticsKey: "your-google-analytics-key-here" -# bioportalAPIKey: "your-bio-portal-api-key-here" -# cesiumToken: "your-cesium-token-here" - - -git: - ## git.enabled set 'true' to create an initContainer and do a git checkout of the metacatui files - ## NOTE: If you set git.enabled: 'false', then you will need to provide values for 'volumes' - ## that - enabled: true - - ## git.repoUrl the https url of the repo to be cloned - repoUrl: "https://github.com/NCEAS/metacatui.git" - ## git.revision can be any string that makes sense after the command `git checkout`... - for - ## example: - ## git checkout tags/2.29.0 => revision: "tags/2.29.0" - ## git checkout develop => revision: "develop" + ## appConfig.theme Corresponds to the name of a directory in src/js/themes/. 'default' if not set ## - revision: "main" + theme: "default" + ## volumes Uncomment and provide values if you want to use a pre-configured PVC instead of doing a ## git checkout #volumes: -# ## volumes.name substitute your own release name, but do NOT change the '-mcui-static-files' part -# - name: -mcui-static-files +# ## volumes.name substitute your own release name, but do NOT change the '-mcui-source-files' part +# - name: -mcui-source-files # persistentVolumeClaim: # claimName: @@ -73,14 +91,14 @@ imagePullSecrets: [] nameOverride: "" fullnameOverride: "" -#livenessProbe: -# httpGet: -# path: / -# port: http -#readinessProbe: -# httpGet: -# path: / -# port: http +livenessProbe: + httpGet: + path: / + port: http +readinessProbe: + httpGet: + path: / + port: http serviceAccount: # Specifies whether a service account should be created @@ -111,16 +129,16 @@ service: type: ClusterIP port: 80 +## ingress typically disabled here and handled by metacat helm chart. Example settings below for +## local dev +## ingress: enabled: false - className: traefik + className: traefik # enable in rancher desktop annotations: - #traefik.ingress.kubernetes.io/router.entrypoints: web kubernetes.io/ingress.class: traefik - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" hosts: - - host: firn.local + - host: firn.local # hostname of local machine paths: - path: / pathType: Prefix @@ -130,9 +148,6 @@ ingress: port: number: 80 tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local resources: {} # We usually recommend not to specify default resources and to leave this as a conscious @@ -146,12 +161,13 @@ resources: {} # cpu: 100m # memory: 128Mi +replicaCount: 1 + autoscaling: enabled: false minReplicas: 1 maxReplicas: 100 targetCPUUtilizationPercentage: 80 - # targetMemoryUtilizationPercentage: 80 nodeSelector: {} From 8e996444d66681358b355f12ec8dd7fd7664d0d1 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Wed, 5 Jun 2024 09:56:21 -0700 Subject: [PATCH 015/169] move pv/pvc yaml --- helm/metacatui-pv.yaml | 14 -------------- helm/metacatui-pvc.yaml | 14 -------------- 2 files changed, 28 deletions(-) delete mode 100644 helm/metacatui-pv.yaml delete mode 100644 helm/metacatui-pvc.yaml diff --git a/helm/metacatui-pv.yaml b/helm/metacatui-pv.yaml deleted file mode 100644 index 689934a4a..000000000 --- a/helm/metacatui-pv.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: PersistentVolume -metadata: - name: metacatui-pv -spec: - accessModes: - - ReadWriteOnce - capacity: - storage: 10Gi - hostPath: - path: /Users/jones/development/metacatui/src - type: "" - persistentVolumeReclaimPolicy: Retain - volumeMode: Filesystem diff --git a/helm/metacatui-pvc.yaml b/helm/metacatui-pvc.yaml deleted file mode 100644 index a478b54b1..000000000 --- a/helm/metacatui-pvc.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: metacatui-pvc - namespace: mcui -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 10Gi - storageClassName: "" - volumeMode: Filesystem - volumeName: metacatui-pv From 8a7fac5f62cabc7cbef0ab9b8caf0338b0c7f3b5 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Wed, 5 Jun 2024 09:57:29 -0700 Subject: [PATCH 016/169] bump chart version --- helm/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 585d738d0..c0ffd53c2f 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.4.0 +version: 0.5.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to From a09abb89e6f8e8e1ba1bc65b3b74fa8c025838b8 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:17:29 -0700 Subject: [PATCH 017/169] set mcui version --- helm/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/Chart.yaml b/helm/Chart.yaml index c0ffd53c2f..7ead3d7aa 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -21,4 +21,4 @@ version: 0.5.0 # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "latest" +appVersion: "2.29.1" From 23261c6a6abebca5cd426a4995b6f3d44777b04b Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:43:53 -0700 Subject: [PATCH 018/169] use mcui version from chart yaml --- helm/templates/deployment.yaml | 10 +- helm/values.yaml | 167 +++++++++++++++------------------ 2 files changed, 84 insertions(+), 93 deletions(-) diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 4e4a141d3..c853d80e7 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -51,10 +51,11 @@ spec: - -c - > start=$(date +%s); - FILENAME={{ .Values.source.package.version }}.zip; + VERSION={{ .Values.source.package.version | default .Chart.AppVersion }} + FILENAME=$VERSION.zip; wget -O ./$FILENAME {{ .Values.source.package.location }}/$FILENAME; unzip $FILENAME -d /tmp/; - mv /tmp/metacatui-{{ .Values.source.package.version }}/* /metacatui/; + mv /tmp/metacatui-$VERSION/* /metacatui/; finish=$(date +%s); echo "$FILENAME download and install took $((finish - start)) sec"; {{- end }} @@ -66,8 +67,9 @@ spec: - name: {{ .Chart.Name }} securityContext: {{- toYaml .Values.securityContext | nindent 12 }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} + image: "{{ .Values.image.repository | default "nginx" }}: + {{- .Values.image.tag | default "latest" }}" + imagePullPolicy: {{ .Values.image.pullPolicy | default "IfNotPresent" }} ports: - name: http containerPort: {{ .Values.service.port }} diff --git a/helm/values.yaml b/helm/values.yaml index 1891947d8..c006a061f 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -1,42 +1,9 @@ -# Default values for metacatui. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. - -source: - ## source.from The source for the metacatui source code. Options are "package", "git", or "pvc" - ## * "package" will download a release package from the metacatui git repository, unzip it, and - ## install the files in local pod storage (emptyDir{}) - ## * "git" will clone a specific branch or tag from the metacatui git repository, and install the - ## files in local pod storage (emptyDir{}) - ## * "pvc" expects to find a pre-configured PVC containing the files to be used. Note you will - ## need to provide values for a pre-configured PVC in 'volumes', below - ## - from: package - - ## git settings ignored unless 'source.from:' is set to 'git' - git: - ## source.git.repoUrl the https url of the repo to be cloned - repoUrl: "https://github.com/NCEAS/metacatui.git" - - ## source.git.revision can be any string that makes sense after the command `git checkout`... - ## - for example: - ## git checkout tags/2.29.0 => revision: "tags/2.29.0" - ## git checkout develop => revision: "develop" - ## - revision: "develop" - - package: - ## source.package.location The remote location where the release zipfile is hosted - ## - location: "https://github.com/NCEAS/metacatui/archive" - - ## source.package.version The release version. Assumes release is a zipfile named .zip - ## example: - ## location: "https://github.com/NCEAS/metacatui/archive" - ## version: "2.29.1" - ## will download: https://github.com/NCEAS/metacatui/archive/2.29.1.zip - ## - version: "2.29.1" +## Default values for metacatui. +## This is a YAML-formatted file. +## Edit values, then install and/or upgrade using: +## +## $ helm upgrade --install releasename -n mynamespace ./relative/path/to/helm/directory +## ## appConfig contains the MetacatUI.AppConfig settings that can be overridden. Uses a configMap to ## define MetacatUI.AppConfig, replacing the version of config/config.js found on the mounted drive. @@ -70,27 +37,9 @@ appConfig: ## appConfig.theme Corresponds to the name of a directory in src/js/themes/. 'default' if not set ## - theme: "default" + theme: "knb" -## volumes Uncomment and provide values if you want to use a pre-configured PVC instead of doing a -## git checkout -#volumes: -# ## volumes.name substitute your own release name, but do NOT change the '-mcui-source-files' part -# - name: -mcui-source-files -# persistentVolumeClaim: -# claimName: - -image: - repository: nginx - pullPolicy: IfNotPresent - # Overrides the image tag whose default is the chart appVersion. - tag: "latest" - -imagePullSecrets: [] -nameOverride: "" -fullnameOverride: "" - livenessProbe: httpGet: path: / @@ -100,37 +49,12 @@ readinessProbe: path: / port: http -serviceAccount: - # Specifies whether a service account should be created - create: false - # Automatically mount a ServiceAccount's API credentials? - automount: true - # Annotations to add to the service account - annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template - name: "" - -podAnnotations: {} -podLabels: {} - -podSecurityContext: {} - # fsGroup: 2000 - -securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - service: type: ClusterIP port: 80 -## ingress typically disabled here and handled by metacat helm chart. Example settings below for -## local dev +## ingress typically disabled here (enabled: false) and handled by metacat helm chart. +## Example settings below for local dev ## ingress: enabled: false @@ -138,17 +62,84 @@ ingress: annotations: kubernetes.io/ingress.class: traefik hosts: - - host: firn.local # hostname of local machine + - host: myMacbookPro.local # example hostname of local machine paths: - path: / pathType: Prefix backend: service: - name: mcui-metacatui # Assumes the service is exposed in the mcui namespace + name: mcui-metacatui # Assumes the service is exposed in the 'mcui' namespace port: number: 80 tls: [] + +## source The source from which to retrieve the metacatui code. NOTE: Changes should not be needed +## here, unless you wish to deviate from the official metacatui release version defined in the +## helm chart (see Chart.yaml) +## +source: + ## source.from Options are "package" (the default), "git", or "pvc" + ## * "package" will download a release package from the metacatui git repository, unzip it, and + ## install the files in local pod storage (emptyDir{}) + ## * "git" will clone a specific branch or tag from the metacatui git repository, and install the + ## files in local pod storage (emptyDir{}) + ## * "pvc" expects to find a pre-configured PVC containing the files to be used. Note you will + ## need to provide values for a pre-configured PVC in 'volumes', below + ## + from: package + + ## source.package (default): use the package version defined in Chart.yaml, unless overridden here + ## Note these settings ignored unless 'source.from:' is set to 'package' + ## example: + ## source.package.location: "https://github.com/NCEAS/metacatui/archive" + ## source.package.version: "2.26.0" + ## ...will download: https://github.com/NCEAS/metacatui/archive/2.26.0.zip + ## + package: + ## source.package.location The remote location where the release zipfile is hosted + ## + location: "https://github.com/NCEAS/metacatui/archive" + + ## source.package.version override the release version defined in Chart.yaml + ## Assumes release is a zipfile named .zip + ## LEAVE COMMENTED UNLESS YOU NEED TO OVERRIDE THE CHART SETTING! + # version: "2.29.1" + + ## source.git clone a specific branch or tag from the metacatui git repository, and install the + ## files in local pod storage (emptyDir{}) + ## Note these settings ignored unless 'source.from:' is set to 'git' + ## +# git: +# ## source.git.repoUrl the https url of the repo to be cloned +# repoUrl: "https://github.com/NCEAS/metacatui.git" +# +# ## source.git.revision can be any string that makes sense after the command `git checkout`... +# ## - for example: +# ## git checkout tags/2.29.0 => revision: "tags/2.29.0" +# ## git checkout develop => revision: "develop" +# ## +# revision: "develop" + +## volumes Uncomment and provide values if you want to use a pre-configured PVC instead of doing a +## git checkout +#volumes: +# ## volumes.name substitute your own release name, but do NOT change the '-mcui-source-files' part +# - name: -mcui-source-files +# persistentVolumeClaim: +# claimName: + + +image: {} +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" +serviceAccount: {} +podAnnotations: {} +podLabels: {} +podSecurityContext: {} +securityContext: {} + resources: {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little @@ -170,7 +161,5 @@ autoscaling: targetCPUUtilizationPercentage: 80 nodeSelector: {} - tolerations: [] - affinity: {} From d14d29e75ddcfa5992abdf66f267b3ba4cb0194d Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:49:53 -0700 Subject: [PATCH 019/169] Update helm/README.md Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- helm/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/helm/README.md b/helm/README.md index caacac8eb..3c3fb2df0 100644 --- a/helm/README.md +++ b/helm/README.md @@ -6,6 +6,7 @@ This is a simple helm chart for debugging a MetacatUI deployment. 1. modify values.yaml as appropriate 2. install the helm chart: + ```shell $ helm -n knb upgrade --install knbmcui ./helm ``` From 8306c1c2d0c665bd12e71a5d425b05eac8dab29d Mon Sep 17 00:00:00 2001 From: Rushiraj Nenuji Date: Thu, 6 Jun 2024 12:23:03 -0700 Subject: [PATCH 020/169] Restore required changes after merging develop Restore required changes after merging develop --- src/js/views/DownloadButtonView.js | 1 - src/js/views/MetadataView.js | 1 - 2 files changed, 2 deletions(-) diff --git a/src/js/views/DownloadButtonView.js b/src/js/views/DownloadButtonView.js index b22d96681..464464fd9 100644 --- a/src/js/views/DownloadButtonView.js +++ b/src/js/views/DownloadButtonView.js @@ -43,7 +43,6 @@ define([ } if ( this.model instanceof PackageModel && - this.nested && (this.model.get("formatType") == "RESOURCE" || this.model.get("type") == "DataPackage" || this.model.get("type") == "Package") diff --git a/src/js/views/MetadataView.js b/src/js/views/MetadataView.js index 4fc208d54..793f43d1b 100644 --- a/src/js/views/MetadataView.js +++ b/src/js/views/MetadataView.js @@ -1122,7 +1122,6 @@ define([ // Add Package Download // create an instance of DownloadButtonView to handle package downloads this.downloadButtonView = new DownloadButtonView({ - id: packageModel.get("id"), model: packageModel, view: "actionsView", }); From 567570c1f2ea8d6640cbdaf1af22ed85a66e4a35 Mon Sep 17 00:00:00 2001 From: Rushiraj Nenuji Date: Thu, 6 Jun 2024 12:36:33 -0700 Subject: [PATCH 021/169] Restore changes from merge conflict Restore changes from merge conflict --- src/js/views/DataPackageView.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/js/views/DataPackageView.js b/src/js/views/DataPackageView.js index 87f054acf..8d63917f2 100644 --- a/src/js/views/DataPackageView.js +++ b/src/js/views/DataPackageView.js @@ -483,6 +483,7 @@ titleTooltip: titleTooltip, downloadUrl: packageUrl, disablePackageDownloads: disablePackageDownloads, + disablePackageUrl: true }); this.$el.append(tableRow); @@ -952,6 +953,12 @@ * @type {null|string} */ packageUrl = null; + /** + * The URL of the nested data package. + * + * @type {null|string} + */ + nestedPackageUrl = null; /** * The members of the data package. @@ -989,6 +996,14 @@ MetacatUI.appModel.get("packageServiceUrl") + encodeURIComponent(dataPackage.id); + // Set the nested package URL + if ( + MetacatUI.appModel.get("viewServiceUrl") !== undefined && + MetacatUI.appModel.get("viewServiceUrl") + ) + nestedPackageUrl = + MetacatUI.appModel.get("viewServiceUrl") + encodeURIComponent(dataPackage.id); + /** * The HTML content for the data package header. * @@ -1000,6 +1015,8 @@ titleTooltip: titleTooltip, disablePackageDownloads: false, downloadUrl: packageUrl, + disablePackageUrl: false, + packageUrl: nestedPackageUrl }); this.$el.append(tableRow); From b9af7478b167f892e7baf2bf2f61aa8e5e2a1d65 Mon Sep 17 00:00:00 2001 From: Rushiraj Nenuji Date: Thu, 6 Jun 2024 12:43:30 -0700 Subject: [PATCH 022/169] Restore changes from merge conflict Restore changes from merge conflict --- src/js/views/DataPackageView.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/js/views/DataPackageView.js b/src/js/views/DataPackageView.js index 87f054acf..241ce98ec 100644 --- a/src/js/views/DataPackageView.js +++ b/src/js/views/DataPackageView.js @@ -983,12 +983,6 @@ title.slice(title.length - 75, title.length) : title; - // Set the package URL - if (MetacatUI.appModel.get("packageServiceUrl")) - packageUrl = - MetacatUI.appModel.get("packageServiceUrl") + - encodeURIComponent(dataPackage.id); - /** * The HTML content for the data package header. * @@ -999,7 +993,6 @@ title: title, titleTooltip: titleTooltip, disablePackageDownloads: false, - downloadUrl: packageUrl, }); this.$el.append(tableRow); @@ -1014,7 +1007,7 @@ this.downloadButtonView.render(); // Add the downloadButtonView el to the span - this.$el.find(".downloadAction").html(this.downloadButtonView.el); + this.$el.find(".downloadAction[data-id='" + dataPackage.id + "']").html(this.downloadButtonView.el); // Filter out the packages from the member list members = _.filter(members, function (m) { From 6f30219439839aa8c461c1a4a81d9022661db07f Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Fri, 7 Jun 2024 16:15:51 -0700 Subject: [PATCH 023/169] adding global values so metacat chart can do overrides --- helm/config/config.js | 8 ++- helm/templates/_helpers.tpl | 18 +++++ helm/values.yaml | 137 +++++++++++++++++++----------------- 3 files changed, 94 insertions(+), 69 deletions(-) diff --git a/helm/config/config.js b/helm/config/config.js index dc05ae99d..349f08f84 100644 --- a/helm/config/config.js +++ b/helm/config/config.js @@ -1,5 +1,5 @@ MetacatUI.AppConfig = { - {{- $ignoreList := list "enabled" "root" "baseUrl" -}} + {{- $ignoreList := list "enabled" "root" "baseUrl" "metacatContext" "d1CNBaseUrl" -}} {{- range $key, $value := .Values.appConfig }} {{- if not (has $key $ignoreList) }} {{- if eq (typeOf $value) "string" }} @@ -10,6 +10,8 @@ MetacatUI.AppConfig = { {{- end }} {{- end -}} {{/* These go last, so we can handle the trailing comma */}} - root: {{ required "root is REQUIRED" .Values.appConfig.root | quote }}, - baseUrl: {{ required "baseUrl is REQUIRED" .Values.appConfig.baseUrl | quote }} + {{- include "metacatui.cn.url" . | nindent 4 }} + root: {{ required "root_is_REQUIRED" .Values.appConfig.root | quote }}, + metacatContext: {{ required "metacatAppContext_is_REQUIRED" .Values.global.metacatAppContext | quote }}, + baseUrl: {{ required "metacatExternalBaseUrl_is_REQUIRED" .Values.global.metacatExternalBaseUrl | quote }} } diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index 823ceb6f9..d7f0d5c79 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -60,3 +60,21 @@ Create the name of the service account to use {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} + +{{/* +Populate the dataone cn url +*/}} +{{- define "metacatui.cn.url" -}} +{{- $d1ClientCnUrl := .Values.global.d1ClientCnUrl }} +{{- if $d1ClientCnUrl }} +{{- if not (hasSuffix "/" $d1ClientCnUrl) -}} + {{- $d1ClientCnUrl = print $d1ClientCnUrl "/" -}} +{{- end -}} +{{- $baseCnURL := regexFind "http.?://[^/]*/" $d1ClientCnUrl }} +{{- if not $baseCnURL }} +d1CNBaseUrl: "ERROR_IN_URL__{{ $d1ClientCnUrl }}", +{{- else }} +d1CNBaseUrl: "{{ $baseCnURL }}", +{{- end }} +{{- end }} +{{- end }} diff --git a/helm/values.yaml b/helm/values.yaml index c006a061f..be87e960a 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -5,41 +5,58 @@ ## $ helm upgrade --install releasename -n mynamespace ./relative/path/to/helm/directory ## -## appConfig contains the MetacatUI.AppConfig settings that can be overridden. Uses a configMap to -## define MetacatUI.AppConfig, replacing the version of config/config.js found on the mounted drive. +global: + ## @param global.metacatExternalBaseUrl metacat base url, accessible from outside the cluster. + ## Include protocol and trailing slash, but not the context; e.g.: "https://test.arcticdata.io/" + ## + metacatExternalBaseUrl: "https://localhost/" + + ## @param global.metacatAppContext The directory that metacat is installed in at the `baseUrl`. + ## Used to populate metacatui's 'metacatContext'. + ## For example, if you have metacat installed in $TOMCAT/webapps/knb, then this should be set + ## to "knb". If you are using the default metacat settings, it should be 'metacat' + ## + metacatAppContext: "metacat" + + ## @param global.d1ClientCnUrl the url of the CN. Used to determine metacatui's 'd1CNBaseUrl' + ## NOTE: only the base URL is used to populate d1CNBaseUrl; anything after the third "/" will be + ## ignored - e.g. if you set d1ClientCnUrl: "https://cn.dataone.org/cn", then d1CNBaseUrl will + ## be set to "https://cn.dataone.org/" + ## + d1ClientCnUrl: "https://cn.dataone.org/cn" + + +## appConfig contains the MetacatUI.AppConfig settings that can be overridden. ## -## You can define any attributes here, that will be automatically inserted into config/config.js. -## For a full list of attributes that can be overridden, see AppModel.js: +## Optional configuration. Note you can define any attributes here, to override those that would +## normally appear in config.js. See full listing in AppModel.js: ## https://github.com/NCEAS/metacatui/blob/main/src/js/models/AppModel.js ## -## Note that changes to the configMap will not be read unless the pod is restarted, and changes to -## these values will not be reflected in the configMap unless you do a 'helm upgrade'. +## * * * IMPORTANT NOTE: * * * DO NOT SET THE FOLLOWING VALUES IN THIS SECTION! * * * +## They will be ignored here, because they are populated from the "global" section, above: +## * baseUrl: uses '.Values.global.metacatExternalBaseUrl' +## * d1CNBaseUrl: uses base URL portion of '.Values.global.d1ClientCnUrl' +## - see above for details +## * metacatContext: uses '.Values.global.metacatAppContext' ## appConfig: - ## appConfig.enabled Use a configMap to define MetacatUI.AppConfig, replacing the on-disk version - ## Typical use is "false" for development purposes, and "true" for more-permanent deployments + ## @param appConfig.enabled Define override values in MetacatUI.AppConfig + ## Since we generally want to avoid loading 2 different config.js files, this would typically be + ## set to "false" for production deployments, and "true" only for development & test environments. ## enabled: true - ## appConfig.root (required) The url root to be appended after the appConfig.baseUrl, below. + ## @param appConfig.root The url root to be appended after the metacatui baseUrl. ## root: "/" - ## appConfig.baseUrl (required) the base url. (Typically defined by the ingress; used to contact - ## the metacat API.) - ## - baseUrl: "http://localhost:8080/" - - ## Optional configuration. Note you can define any attributes here, to override those that would - ## normally appear in config.js. See full listing in AppModel.js: - ## https://github.com/NCEAS/metacatui/blob/main/src/js/models/AppModel.js - ## - - ## appConfig.theme Corresponds to the name of a directory in src/js/themes/. 'default' if not set + ## @param appConfig.theme Corresponds to the name of a directory in src/js/themes/. + ## Uses the 'default' theme if not set ## theme: "knb" - +## Probes +## livenessProbe: httpGet: path: / @@ -53,7 +70,7 @@ service: type: ClusterIP port: 80 -## ingress typically disabled here (enabled: false) and handled by metacat helm chart. +## @param ingress typically disabled here (enabled: false) and handled by metacat helm chart. ## Example settings below for local dev ## ingress: @@ -74,12 +91,12 @@ ingress: tls: [] -## source The source from which to retrieve the metacatui code. NOTE: Changes should not be needed -## here, unless you wish to deviate from the official metacatui release version defined in the -## helm chart (see Chart.yaml) +## @param source The source from which to retrieve the metacatui code. NOTE: Changes should not be +## needed here, unless you wish to deviate from the official metacatui release version defined in +## the helm chart (see Chart.yaml) ## source: - ## source.from Options are "package" (the default), "git", or "pvc" + ## @param source.from Options are "package" (the default), "git", or "pvc" ## * "package" will download a release package from the metacatui git repository, unzip it, and ## install the files in local pod storage (emptyDir{}) ## * "git" will clone a specific branch or tag from the metacatui git repository, and install the @@ -89,7 +106,7 @@ source: ## from: package - ## source.package (default): use the package version defined in Chart.yaml, unless overridden here + ## @param source.package (default): Download the official release version defined in Chart.yaml ## Note these settings ignored unless 'source.from:' is set to 'package' ## example: ## source.package.location: "https://github.com/NCEAS/metacatui/archive" @@ -97,31 +114,31 @@ source: ## ...will download: https://github.com/NCEAS/metacatui/archive/2.26.0.zip ## package: - ## source.package.location The remote location where the release zipfile is hosted + ## @param source.package.location The remote location where the release zipfile is hosted ## location: "https://github.com/NCEAS/metacatui/archive" ## source.package.version override the release version defined in Chart.yaml ## Assumes release is a zipfile named .zip ## LEAVE COMMENTED UNLESS YOU NEED TO OVERRIDE THE CHART SETTING! - # version: "2.29.1" +# version: "2.29.1" - ## source.git clone a specific branch or tag from the metacatui git repository, and install the - ## files in local pod storage (emptyDir{}) + ## @param source.git clone a specific branch or tag from the metacatui git repository, and install + ## the files in local pod storage (emptyDir{}) ## Note these settings ignored unless 'source.from:' is set to 'git' ## -# git: -# ## source.git.repoUrl the https url of the repo to be cloned -# repoUrl: "https://github.com/NCEAS/metacatui.git" -# -# ## source.git.revision can be any string that makes sense after the command `git checkout`... -# ## - for example: -# ## git checkout tags/2.29.0 => revision: "tags/2.29.0" -# ## git checkout develop => revision: "develop" -# ## -# revision: "develop" - -## volumes Uncomment and provide values if you want to use a pre-configured PVC instead of doing a + git: + ## @param source.git.repoUrl the https url of the repo to be cloned + repoUrl: "https://github.com/NCEAS/metacatui.git" + + ## @param source.git.revision Any string that makes sense after the command `git checkout`... + ## - for example: + ## revision: "tags/2.29.0" => git checkout tags/2.29.0 + ## revision: "develop" => git checkout develop + ## + revision: "develop" + +## volumes: Uncomment and provide values if you want to use a pre-configured PVC instead of doing a ## git checkout #volumes: # ## volumes.name substitute your own release name, but do NOT change the '-mcui-source-files' part @@ -129,29 +146,6 @@ source: # persistentVolumeClaim: # claimName: - -image: {} -imagePullSecrets: [] -nameOverride: "" -fullnameOverride: "" -serviceAccount: {} -podAnnotations: {} -podLabels: {} -podSecurityContext: {} -securityContext: {} - -resources: {} - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - replicaCount: 1 autoscaling: @@ -163,3 +157,14 @@ autoscaling: nodeSelector: {} tolerations: [] affinity: {} + +image: {} +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" +serviceAccount: {} +podAnnotations: {} +podLabels: {} +podSecurityContext: {} +securityContext: {} +resources: {} From ccb643e4c3679a8767ed1fddcfe40adb5ac0a017 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Fri, 7 Jun 2024 18:38:52 -0700 Subject: [PATCH 024/169] mount configmap containing install script --- helm/config/install.sh | 168 +++++++++++++++++++++++++++++++++ helm/templates/deployment.yaml | 4 + 2 files changed, 172 insertions(+) create mode 100644 helm/config/install.sh diff --git a/helm/config/install.sh b/helm/config/install.sh new file mode 100644 index 000000000..dbcaed504 --- /dev/null +++ b/helm/config/install.sh @@ -0,0 +1,168 @@ +#!/bin/bash +# A script that installs MetacatUI + +# EXPECTS THESE FOLLOWING ENV VARS TO BE PRE-SET +# REQUIRED: +# * tag - MetacatUI tag/version or branch name - e.g. 2.29.1 +# * hostedRepoTheme - boolean +# * configFile +# * updateConfigPath +# * documentRootDir +# +# OPTIONAL: +# * themeLocation + +# Remove the old backup and backup the currently deployed MetacatUI +echo -e "Backing up MetacatUI...." +sudo rm -rf ~/ui-bak; +sudo cp -rf /var/www/$documentRootDir ~/ui-bak; + +# Get the latest MetacatUI release and unzip it +echo -e "Downloading MetacatUI $tag...." +curl -LO https://github.com/NCEAS/metacatui/archive/$tag.zip; +mv $tag.zip ~/; +unzip ~/$tag.zip -d ~/; + +echo -e "Deploying MetacatUI $tag...." +# Configure MetacatUI +if $updateConfigPath ; +then + # Update the config file path, for certain deployments (usually only production environment) + oldPath="\/config\/config.js" + sed "s/$oldPath/"${configFile//\//\\/}"/g" ~/metacatui-$tag/src/index.html > ~/metacatui-$tag/src/index.html.2; + mv ~/metacatui-$tag/src/index.html.2 ~/metacatui-$tag/src/index.html; + + # If a hosted repo theme is specified, retrieve it + if $hostedRepoTheme ; + then + # Clone the hosted-repositories repo + git clone https://github.nceas.ucsb.edu/dataone/hosted-repositories.git; + # Copy the theme directory to MetacatUI + cp -rf hosted-repositories/$themeLocation ~/metacatui-$tag/src/js/themes/; + #Remove the hosted-repositories git directory + rm -rf hosted-repositories + fi + +else + # Copy the config file to the MetacatUI config directory + cp ../$configFile ~/metacatui-$tag/src/config/config.js; +fi + +#Copy the MetacatUI src to the deployment location +sudo cp -rf ~/metacatui-$tag/src/* /var/www/$documentRootDir/; + + + + +#################################################################################################### +## FOR REFERENCE ONLY: +## +#case $deployment in +# +# knb) +# configFile="/js/themes/knb/config.js" +# updateConfigPath=true +# documentRootDir="org.ecoinformatics.knb/metacatui" +# echo -e "Upgrading KNB production" +# ;; +# +# arctic) +# configFile="/catalog/js/themes/arctic/config.js" +# updateConfigPath=true +# documentRootDir="arcticdata.io/htdocs/catalog" +# echo -e "Upgrading Arctic Data production" +# ;; +# +# dataone) +# configFile="/js/themes/dataone/config.js" +# updateConfigPath=true +# documentRootDir="search.dataone.org" +# echo -e "Upgrading DataONE production" +# ;; +# +# opc) +# configFile="/js/themes/opc/config.js" +# updateConfigPath=true +# documentRootDir="opc.dataone.org" +# hostedRepoTheme=true +# themeLocation="src/opc/js/themes/opc" +# echo -e "Upgrading OPC production" +# ;; +# +# cerp) +# configFile="/js/themes/cerp/config.js" +# updateConfigPath=true +# documentRootDir="cerp-sfwmd.dataone.org" +# hostedRepoTheme=true +# themeLocation="src/cerp/js/themes/cerp" +# echo -e "Upgrading CERP-SFWMD production" +# ;; +# +# drp) +# configFile="/js/themes/drp/config.js" +# updateConfigPath=true +# documentRootDir="drp.dataone.org" +# hostedRepoTheme=true +# themeLocation="src/drp/js/themes/drp" +# echo -e "Upgrading DRP production" +# ;; +# +# dev.nceas) +# configFile="dev.nceas.js" +# documentRootDir="edu.ucsb.nceas.dev" +# echo -e "Upgrading dev.nceas" +# ;; +# +# test.arcticdata) +# configFile="test.arcticdata.js" +# documentRootDir="test.arcticdata.io" +# echo -e "Upgrading test.arcticdata.io" +# ;; +# +# demo.arcticdata) +# configFile="demo.arcticdata.js" +# documentRootDir="demo.arcticdata.io" +# echo -e "Upgrading demo.arcticdata.io" +# ;; +# +# demo.nceas) +# configFile="dev.nceas.js" +# documentRootDir="demo.nceas.ucsb.edu" +# echo -e "Upgrading demo.nceas" +# ;; +# +# search-sandbox) +# configFile="search-sandbox.js" +# documentRootDir="search-sandbox.test.dataone.org" +# echo -e "Upgrading search-sandbox.test.dataone.org" +# ;; +# +# search-stage) +# configFile="search-stage.js" +# documentRootDir="search-stage.test.dataone.org" +# echo -e "Upgrading search-stage.test.dataone.org" +# ;; +# +# search.test) +# configFile="search.test.js" +# documentRootDir="search.test.dataone.org" +# echo -e "Upgrading search.test.dataone.org" +# ;; +# +# search-demo) +# configFile="search-demo.js" +# documentRootDir="search-demo.dataone.org" +# echo -e "Upgrading search-demo.dataone.org" +# ;; +# +# plus-preview-fancy-vulture) +# configFile="dataone-plus-preview.js" +# documentRootDir="fancy-vulture.nceas.ucsb.edu" +# echo -e "Upgrading Fancy Vulture to DataONE Plus Preview Production" +# ;; +# +# *) +# echo -e "Deployment unknown. Please upgrade MetacatUI manually." +# exit +# ;; +#esac diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index c853d80e7..f2ea095c5 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -51,6 +51,7 @@ spec: - -c - > start=$(date +%s); + # # # # # # TODO - RUN THE MOUNTED /TMP/SCRIPT.SH INSTEAD # # # # # # VERSION={{ .Values.source.package.version | default .Chart.AppVersion }} FILENAME=$VERSION.zip; wget -O ./$FILENAME {{ .Values.source.package.location }}/$FILENAME; @@ -62,6 +63,9 @@ spec: volumeMounts: - name: {{ .Release.Name }}-mcui-source-files mountPath: /metacatui + - name: {{ .Release.Name }}-mcui-config-vol + subPath: install.sh + mountPath: /tmp/install.sh {{- end }} containers: - name: {{ .Chart.Name }} From 1dade31a719858e83af173a32a6d0b66b2687924 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 17 Jun 2024 13:20:02 -0700 Subject: [PATCH 025/169] last checkin before implementing script installation --- helm/config/install.sh | 2 +- helm/templates/configmap.yaml | 2 +- helm/templates/deployment.yaml | 6 +++--- helm/values.yaml | 25 ++++++++++++++----------- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/helm/config/install.sh b/helm/config/install.sh index dbcaed504..0539ad8f4 100644 --- a/helm/config/install.sh +++ b/helm/config/install.sh @@ -1,7 +1,7 @@ #!/bin/bash # A script that installs MetacatUI -# EXPECTS THESE FOLLOWING ENV VARS TO BE PRE-SET +# EXPECTS THE FOLLOWING ENV VARS TO BE PRE-SET # REQUIRED: # * tag - MetacatUI tag/version or branch name - e.g. 2.29.1 # * hostedRepoTheme - boolean diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml index 55948e4d0..965830222 100644 --- a/helm/templates/configmap.yaml +++ b/helm/templates/configmap.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: ConfigMap metadata: - name: {{ .Release.Name }}-metacatui-configjs + name: {{ .Release.Name }}-metacatui-configfiles labels: {{- include "metacatui.labels" . | nindent 4 }} data: diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index f2ea095c5..137f71348 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -51,8 +51,8 @@ spec: - -c - > start=$(date +%s); - # # # # # # TODO - RUN THE MOUNTED /TMP/SCRIPT.SH INSTEAD # # # # # # - VERSION={{ .Values.source.package.version | default .Chart.AppVersion }} + echo "Starting at $start"; + VERSION={{ .Values.source.package.version | default .Chart.AppVersion }}; FILENAME=$VERSION.zip; wget -O ./$FILENAME {{ .Values.source.package.location }}/$FILENAME; unzip $FILENAME -d /tmp/; @@ -104,7 +104,7 @@ spec: {{- if .Values.appConfig.enabled }} - name: {{ .Release.Name }}-mcui-config-vol configMap: - name: {{ .Release.Name }}-metacatui-configjs + name: {{ .Release.Name }}-metacatui-configfiles defaultMode: 0644 {{- end }} {{- if not .Values.source.pvc }} diff --git a/helm/values.yaml b/helm/values.yaml index be87e960a..a17db8fae 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -57,14 +57,14 @@ appConfig: ## Probes ## -livenessProbe: - httpGet: - path: / - port: http -readinessProbe: - httpGet: - path: / - port: http +#livenessProbe: +# httpGet: +# path: / +# port: http +#readinessProbe: +# httpGet: +# path: / +# port: http service: type: ClusterIP @@ -75,11 +75,11 @@ service: ## ingress: enabled: false - className: traefik # enable in rancher desktop + className: traefik # enable in rancher desktop annotations: kubernetes.io/ingress.class: traefik hosts: - - host: myMacbookPro.local # example hostname of local machine + - host: myMacbookPro.local # example hostname of local machine paths: - path: / pathType: Prefix @@ -109,6 +109,7 @@ source: ## @param source.package (default): Download the official release version defined in Chart.yaml ## Note these settings ignored unless 'source.from:' is set to 'package' ## example: + ## source.from: package ## source.package.location: "https://github.com/NCEAS/metacatui/archive" ## source.package.version: "2.26.0" ## ...will download: https://github.com/NCEAS/metacatui/archive/2.26.0.zip @@ -121,7 +122,7 @@ source: ## source.package.version override the release version defined in Chart.yaml ## Assumes release is a zipfile named .zip ## LEAVE COMMENTED UNLESS YOU NEED TO OVERRIDE THE CHART SETTING! -# version: "2.29.1" + # version: "2.29.1" ## @param source.git clone a specific branch or tag from the metacatui git repository, and install ## the files in local pod storage (emptyDir{}) @@ -140,6 +141,8 @@ source: ## volumes: Uncomment and provide values if you want to use a pre-configured PVC instead of doing a ## git checkout +## Note these settings ignored unless 'source.from:' is set to 'pvc' +## #volumes: # ## volumes.name substitute your own release name, but do NOT change the '-mcui-source-files' part # - name: -mcui-source-files From e7c1bd5f6ad2654eb531db7a6785fe7e392b592a Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Wed, 19 Jun 2024 11:13:11 -0700 Subject: [PATCH 026/169] not used --- helm/config/install.sh | 168 ----------------------------------------- 1 file changed, 168 deletions(-) delete mode 100644 helm/config/install.sh diff --git a/helm/config/install.sh b/helm/config/install.sh deleted file mode 100644 index 0539ad8f4..000000000 --- a/helm/config/install.sh +++ /dev/null @@ -1,168 +0,0 @@ -#!/bin/bash -# A script that installs MetacatUI - -# EXPECTS THE FOLLOWING ENV VARS TO BE PRE-SET -# REQUIRED: -# * tag - MetacatUI tag/version or branch name - e.g. 2.29.1 -# * hostedRepoTheme - boolean -# * configFile -# * updateConfigPath -# * documentRootDir -# -# OPTIONAL: -# * themeLocation - -# Remove the old backup and backup the currently deployed MetacatUI -echo -e "Backing up MetacatUI...." -sudo rm -rf ~/ui-bak; -sudo cp -rf /var/www/$documentRootDir ~/ui-bak; - -# Get the latest MetacatUI release and unzip it -echo -e "Downloading MetacatUI $tag...." -curl -LO https://github.com/NCEAS/metacatui/archive/$tag.zip; -mv $tag.zip ~/; -unzip ~/$tag.zip -d ~/; - -echo -e "Deploying MetacatUI $tag...." -# Configure MetacatUI -if $updateConfigPath ; -then - # Update the config file path, for certain deployments (usually only production environment) - oldPath="\/config\/config.js" - sed "s/$oldPath/"${configFile//\//\\/}"/g" ~/metacatui-$tag/src/index.html > ~/metacatui-$tag/src/index.html.2; - mv ~/metacatui-$tag/src/index.html.2 ~/metacatui-$tag/src/index.html; - - # If a hosted repo theme is specified, retrieve it - if $hostedRepoTheme ; - then - # Clone the hosted-repositories repo - git clone https://github.nceas.ucsb.edu/dataone/hosted-repositories.git; - # Copy the theme directory to MetacatUI - cp -rf hosted-repositories/$themeLocation ~/metacatui-$tag/src/js/themes/; - #Remove the hosted-repositories git directory - rm -rf hosted-repositories - fi - -else - # Copy the config file to the MetacatUI config directory - cp ../$configFile ~/metacatui-$tag/src/config/config.js; -fi - -#Copy the MetacatUI src to the deployment location -sudo cp -rf ~/metacatui-$tag/src/* /var/www/$documentRootDir/; - - - - -#################################################################################################### -## FOR REFERENCE ONLY: -## -#case $deployment in -# -# knb) -# configFile="/js/themes/knb/config.js" -# updateConfigPath=true -# documentRootDir="org.ecoinformatics.knb/metacatui" -# echo -e "Upgrading KNB production" -# ;; -# -# arctic) -# configFile="/catalog/js/themes/arctic/config.js" -# updateConfigPath=true -# documentRootDir="arcticdata.io/htdocs/catalog" -# echo -e "Upgrading Arctic Data production" -# ;; -# -# dataone) -# configFile="/js/themes/dataone/config.js" -# updateConfigPath=true -# documentRootDir="search.dataone.org" -# echo -e "Upgrading DataONE production" -# ;; -# -# opc) -# configFile="/js/themes/opc/config.js" -# updateConfigPath=true -# documentRootDir="opc.dataone.org" -# hostedRepoTheme=true -# themeLocation="src/opc/js/themes/opc" -# echo -e "Upgrading OPC production" -# ;; -# -# cerp) -# configFile="/js/themes/cerp/config.js" -# updateConfigPath=true -# documentRootDir="cerp-sfwmd.dataone.org" -# hostedRepoTheme=true -# themeLocation="src/cerp/js/themes/cerp" -# echo -e "Upgrading CERP-SFWMD production" -# ;; -# -# drp) -# configFile="/js/themes/drp/config.js" -# updateConfigPath=true -# documentRootDir="drp.dataone.org" -# hostedRepoTheme=true -# themeLocation="src/drp/js/themes/drp" -# echo -e "Upgrading DRP production" -# ;; -# -# dev.nceas) -# configFile="dev.nceas.js" -# documentRootDir="edu.ucsb.nceas.dev" -# echo -e "Upgrading dev.nceas" -# ;; -# -# test.arcticdata) -# configFile="test.arcticdata.js" -# documentRootDir="test.arcticdata.io" -# echo -e "Upgrading test.arcticdata.io" -# ;; -# -# demo.arcticdata) -# configFile="demo.arcticdata.js" -# documentRootDir="demo.arcticdata.io" -# echo -e "Upgrading demo.arcticdata.io" -# ;; -# -# demo.nceas) -# configFile="dev.nceas.js" -# documentRootDir="demo.nceas.ucsb.edu" -# echo -e "Upgrading demo.nceas" -# ;; -# -# search-sandbox) -# configFile="search-sandbox.js" -# documentRootDir="search-sandbox.test.dataone.org" -# echo -e "Upgrading search-sandbox.test.dataone.org" -# ;; -# -# search-stage) -# configFile="search-stage.js" -# documentRootDir="search-stage.test.dataone.org" -# echo -e "Upgrading search-stage.test.dataone.org" -# ;; -# -# search.test) -# configFile="search.test.js" -# documentRootDir="search.test.dataone.org" -# echo -e "Upgrading search.test.dataone.org" -# ;; -# -# search-demo) -# configFile="search-demo.js" -# documentRootDir="search-demo.dataone.org" -# echo -e "Upgrading search-demo.dataone.org" -# ;; -# -# plus-preview-fancy-vulture) -# configFile="dataone-plus-preview.js" -# documentRootDir="fancy-vulture.nceas.ucsb.edu" -# echo -e "Upgrading Fancy Vulture to DataONE Plus Preview Production" -# ;; -# -# *) -# echo -e "Deployment unknown. Please upgrade MetacatUI manually." -# exit -# ;; -#esac From a1ba8be5db1ad7434bb9402fd0c8218414ab030c Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Wed, 19 Jun 2024 11:15:38 -0700 Subject: [PATCH 027/169] require theme; make config.js optional --- helm/config/config.js | 2 +- helm/templates/configmap.yaml | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/helm/config/config.js b/helm/config/config.js index 349f08f84..a3f86477c 100644 --- a/helm/config/config.js +++ b/helm/config/config.js @@ -9,8 +9,8 @@ MetacatUI.AppConfig = { {{- end }} {{- end }} {{- end -}} - {{/* These go last, so we can handle the trailing comma */}} {{- include "metacatui.cn.url" . | nindent 4 }} + theme: {{ required "theme_is_REQUIRED" .Values.appConfig.theme | quote }}, root: {{ required "root_is_REQUIRED" .Values.appConfig.root | quote }}, metacatContext: {{ required "metacatAppContext_is_REQUIRED" .Values.global.metacatAppContext | quote }}, baseUrl: {{ required "metacatExternalBaseUrl_is_REQUIRED" .Values.global.metacatExternalBaseUrl | quote }} diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml index 965830222..1ec251b47 100644 --- a/helm/templates/configmap.yaml +++ b/helm/templates/configmap.yaml @@ -1,9 +1,11 @@ +{{- if .Values.appConfig.enabled }} # Load all files in the "config" directory into a ConfigMap apiVersion: v1 kind: ConfigMap metadata: - name: {{ .Release.Name }}-metacatui-configfiles + name: {{ .Release.Name }}-metacatui-config-js labels: {{- include "metacatui.labels" . | nindent 4 }} data: -{{ (tpl (.Files.Glob "config/*").AsConfig . ) | nindent 4 }} + {{- (tpl (.Files.Glob "config/config.js").AsConfig . ) | nindent 2 }} +{{- end }} From 8586e63a762b3134744c6a55e9c79f387e832dbc Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Wed, 19 Jun 2024 11:26:08 -0700 Subject: [PATCH 028/169] change /config/config.js to config/config.js --- src/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.html b/src/index.html index d3170845e..972d48e4c 100644 --- a/src/index.html +++ b/src/index.html @@ -4,7 +4,7 @@ From 2f97b4ab29fa7456df85c2b8d7453d67169ca90c Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Wed, 19 Jun 2024 11:46:36 -0700 Subject: [PATCH 029/169] handle different roots; autoconfig probes or make optional --- helm/templates/_helpers.tpl | 19 ++++++++- helm/templates/deployment.yaml | 43 ++++++++++++-------- helm/values.yaml | 72 +++++++++++++++++++++++++--------- 3 files changed, 97 insertions(+), 37 deletions(-) diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index d7f0d5c79..b875fbaef 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -71,10 +71,25 @@ Populate the dataone cn url {{- $d1ClientCnUrl = print $d1ClientCnUrl "/" -}} {{- end -}} {{- $baseCnURL := regexFind "http.?://[^/]*/" $d1ClientCnUrl }} -{{- if not $baseCnURL }} +{{- if not $baseCnURL -}} d1CNBaseUrl: "ERROR_IN_URL__{{ $d1ClientCnUrl }}", -{{- else }} +{{- else -}} d1CNBaseUrl: "{{ $baseCnURL }}", {{- end }} {{- end }} {{- end }} + +{{/* +Remove trailing slash from root, if it exists +*/}} +{{- define "metacatui.clean.root" -}} +{{- $cleanedRoot := regexReplaceAll "/$" .Values.appConfig.root "" -}} +{{- $cleanedRoot }} +{{- end }} + +{{/* +generate file path for the web root mount +*/}} +{{- define "metacatui.root.mountpath" -}} +/usr/share/nginx/html{{ include "metacatui.clean.root" . }} +{{- end }} diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 137f71348..71911c202 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -56,16 +56,14 @@ spec: FILENAME=$VERSION.zip; wget -O ./$FILENAME {{ .Values.source.package.location }}/$FILENAME; unzip $FILENAME -d /tmp/; - mv /tmp/metacatui-$VERSION/* /metacatui/; + mkdir -p /metacatui{{ include "metacatui.clean.root" . }}; + mv /tmp/metacatui-$VERSION/src/* /metacatui{{ include "metacatui.clean.root" . }}; finish=$(date +%s); echo "$FILENAME download and install took $((finish - start)) sec"; {{- end }} volumeMounts: - name: {{ .Release.Name }}-mcui-source-files mountPath: /metacatui - - name: {{ .Release.Name }}-mcui-config-vol - subPath: install.sh - mountPath: /tmp/install.sh {{- end }} containers: - name: {{ .Chart.Name }} @@ -78,35 +76,46 @@ spec: - name: http containerPort: {{ .Values.service.port }} protocol: TCP - {{- with .Values.livenessProbe }} + {{- if .Values.livenessProbeEnabled }} livenessProbe: + {{- if .Values.livenessProbe }} + {{- with .Values.livenessProbe }} {{- toYaml . | nindent 12 }} + {{- end }} + {{- else }} + httpGet: + path: {{ .Values.appConfig.root }} + port: http + {{- end }} {{- end }} - {{- with .Values.readinessProbe }} + {{- if .Values.readinessProbeEnabled }} readinessProbe: + {{- if .Values.readinessProbe }} + {{- with .Values.readinessProbe }} {{- toYaml . | nindent 12 }} + {{- end }} + {{- else }} + httpGet: + path: {{ .Values.appConfig.root }} + port: http + {{- end }} {{- end }} resources: {{- toYaml .Values.resources | nindent 12 }} volumeMounts: - {{- if .Values.appConfig.enabled }} - - name: {{ .Release.Name }}-mcui-config-vol - mountPath: /usr/share/nginx/html/config/config.js - subPath: config.js - {{- end }} - name: {{ .Release.Name }}-mcui-source-files - mountPath: "/usr/share/nginx/html" - subPath: "src" + mountPath: /usr/share/nginx/html + - name: {{ .Release.Name }}-mcui-config-js + mountPath: {{ include "metacatui.root.mountpath" . }}/config/config.js + subPath: config.js {{- with .Values.volumeMounts }} {{- toYaml . | nindent 12 }} {{- end }} volumes: - {{- if .Values.appConfig.enabled }} - - name: {{ .Release.Name }}-mcui-config-vol + - name: {{ .Release.Name }}-mcui-config-js configMap: - name: {{ .Release.Name }}-metacatui-configfiles + name: {{ .Release.Name }}-metacatui-config-js defaultMode: 0644 - {{- end }} {{- if not .Values.source.pvc }} - name: {{ .Release.Name }}-mcui-source-files emptyDir: {} diff --git a/helm/values.yaml b/helm/values.yaml index a17db8fae..90479ece2 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -26,45 +26,81 @@ global: d1ClientCnUrl: "https://cn.dataone.org/cn" -## appConfig contains the MetacatUI.AppConfig settings that can be overridden. +## @section MetacatUI Configuration Files ('config.js') ## -## Optional configuration. Note you can define any attributes here, to override those that would -## normally appear in config.js. See full listing in AppModel.js: +## The k8s version of Metacatui requires two 'config.js' configuration files: +## 1. the "root config" at the path {root}/config/config.js, and +## 2. the "theme config" in the theme directory itself (for example, see the knb config.js at: +## https://github.com/NCEAS/metacatui/blob/main/src/js/themes/knb/config.js) +## +## The "root config" file must, at a minimum, contain the name of the theme to be used, e.g: +## +## MetacatUI.AppConfig = { +## theme: "knb" +## }; +## +## ...and metacatui will then load that theme and its "theme config". +## If any additional settings are defined in the "root config", Metacatui will use them to override +## corresponding settings in the "theme config". +## +## By default, this chart creates a simple "root config" that contains any values of the form: +## 'key: stringValue,' or 'key: intValue,' provided in the 'appConfig:' section below. If you need +## to provide more-complex overrides, set 'appConfig.enabled: false', below, and manually create +## your own configMap named 'YourReleaseName-metacatui-config-js', containing your custom config.js. +## +## Note that if you choose not to use one of the themes that are bundled with metacatui, you will +## need to provide the files for that theme (including its 'theme config' file) on a Persistent +## Volume. See the MetacatUI documentation for help with creating custom themes: +## https://nceas.github.io/metacatui/install/configuration/index.html +## + +## @param appConfig The MetacatUI.AppConfig optional override settings +## +## Optional configuration. Note you can define any attributes here, to override those that appear +## in config.js, provided they are of the form: 'key: stringValue,' or 'key: intValue,'. +## +## See full listing in AppModel.js: ## https://github.com/NCEAS/metacatui/blob/main/src/js/models/AppModel.js ## ## * * * IMPORTANT NOTE: * * * DO NOT SET THE FOLLOWING VALUES IN THIS SECTION! * * * ## They will be ignored here, because they are populated from the "global" section, above: ## * baseUrl: uses '.Values.global.metacatExternalBaseUrl' ## * d1CNBaseUrl: uses base URL portion of '.Values.global.d1ClientCnUrl' -## - see above for details ## * metacatContext: uses '.Values.global.metacatAppContext' ## appConfig: - ## @param appConfig.enabled Define override values in MetacatUI.AppConfig - ## Since we generally want to avoid loading 2 different config.js files, this would typically be - ## set to "false" for production deployments, and "true" only for development & test environments. + ## @param appConfig.enabled Define theme name and override values in MetacatUI.AppConfig + ## If you need to provide more-complex overrides, set 'appConfig.enabled: false', and manually + ## create your own configMap named 'YourReleaseName-metacatui-config-js', containing your custom + ## config.js. ## enabled: true - ## @param appConfig.root The url root to be appended after the metacatui baseUrl. + ## @param appConfig.root The url root to be appended after the metacatui baseUrl. Starts with "/" ## root: "/" ## @param appConfig.theme Corresponds to the name of a directory in src/js/themes/. - ## Uses the 'default' theme if not set ## theme: "knb" -## Probes +## @param livenessProbeEnabled Enable livenessProbe. Autoconfigured using .Values.appConfig.root +## To override autoconfig, keep 'livenessProbeEnabled: true', and define a livenessProbe; e.g: +## livenessProbe: +## httpGet: +## path: /myPath +## port: http +## +livenessProbeEnabled: true + +## @param readinessProbeEnabled Enable readinessProbe. Autoconfigured using .Values.appConfig.root +## To override autoconfig, keep 'readinessProbeEnabled: true', and define a readinessProbe; e.g: +## readinessProbe: +## httpGet: +## path: /myPath +## port: http ## -#livenessProbe: -# httpGet: -# path: / -# port: http -#readinessProbe: -# httpGet: -# path: / -# port: http +readinessProbeEnabled: true service: type: ClusterIP From 0fdc3b7d70b29eab4b8da81d32f52fcf02e20e76 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Wed, 19 Jun 2024 14:39:08 -0700 Subject: [PATCH 030/169] fix initContainer --- helm/templates/_helpers.tpl | 18 +++++++ helm/templates/deployment.yaml | 53 +++++++++++++-------- helm/values.yaml | 86 +++++++++++++++++----------------- 3 files changed, 95 insertions(+), 62 deletions(-) diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index b875fbaef..09204bdf9 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -93,3 +93,21 @@ generate file path for the web root mount {{- define "metacatui.root.mountpath" -}} /usr/share/nginx/html{{ include "metacatui.clean.root" . }} {{- end }} + +{{/* +validate and clean up '.Values.source.from' +*/}} +{{- define "metacatui.source.from" -}} +{{- $source := "" }} +{{- $defaultSrc := "package" }} +{{- if not (and .Values.source .Values.source.from) }} +{{- $source = $defaultSrc }} +{{- else }} +{{- $source = .Values.source.from }} +{{- end }} +{{- $allowedSourceVals := list "package" "git" "pvc" }} +{{- if not (has $source $allowedSourceVals) }} +{{- $source = $defaultSrc }} +{{- end }} +{{- $source }} +{{- end }} diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 71911c202..d1ce2edd0 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -30,37 +30,52 @@ spec: serviceAccountName: {{ include "metacatui.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} - {{- if not (eq .Values.source.from "pvc") }} + {{- $source := (include "metacatui.source.from" .) }} + {{- if ne $source "pvc" }} initContainers: - {{- if (eq .Values.source.from "git") }} - - name: git-clone - image: alpine/git:latest - command: - - sh - - -c - - > - start=$(date +%s); - git clone -b {{ .Values.source.git.revision }} --depth 1 {{ .Values.source.git.repoUrl }} /metacatui; - finish=$(date +%s); - echo "git clone -b {{ .Values.source.git.revision }} took $((finish - start)) sec"; - {{- else }} - name: get-source + {{- if eq $source "git" }} + image: alpine/git:latest + {{- else }} image: busybox:latest + {{- end }} command: - sh - -c - > start=$(date +%s); echo "Starting at $start"; - VERSION={{ .Values.source.package.version | default .Chart.AppVersion }}; + DEST="/metacatui{{ include "metacatui.clean.root" . }}"; + mkdir -p $DEST; + {{- if eq $source "git" }} + {{- $repoUrl := "" }} + {{- $revision := "" }} + {{- if and .Values.source .Values.source.git }} + {{- $repoUrl = .Values.source.git.repoUrl }} + {{- $revision = .Values.source.git.revision }} + {{- end }} + REPO='{{ $repoUrl | default "https://github.com/NCEAS/metacatui.git" }}'; + REV='{{ $revision | default .Chart.AppVersion }}'; + git clone -b $REV --depth 1 $REPO /tmp/metacatui/; + mv /tmp/metacatui/src/* $DEST; + finish=$(date +%s); + echo "git clone -b {{ $revision }} --depth 1 took $((finish - start)) sec"; + {{- else }} + {{- $version := "" }} + {{- $location := "" }} + {{- if and .Values.source .Values.source.package }} + {{- $version = .Values.source.package.version }} + {{- $location = .Values.source.package.location }} + {{- end }} + VERSION={{ $version | default .Chart.AppVersion }}; FILENAME=$VERSION.zip; - wget -O ./$FILENAME {{ .Values.source.package.location }}/$FILENAME; + LOC='{{ $location | default "https://github.com/NCEAS/metacatui/archive" }}'; + wget -O ./$FILENAME $LOC/$FILENAME; unzip $FILENAME -d /tmp/; - mkdir -p /metacatui{{ include "metacatui.clean.root" . }}; - mv /tmp/metacatui-$VERSION/src/* /metacatui{{ include "metacatui.clean.root" . }}; + mv /tmp/metacatui-$VERSION/src/* $DEST; finish=$(date +%s); echo "$FILENAME download and install took $((finish - start)) sec"; - {{- end }} + {{- end }} volumeMounts: - name: {{ .Release.Name }}-mcui-source-files mountPath: /metacatui @@ -116,7 +131,7 @@ spec: configMap: name: {{ .Release.Name }}-metacatui-config-js defaultMode: 0644 - {{- if not .Values.source.pvc }} + {{- if ne $source "pvc" }} - name: {{ .Release.Name }}-mcui-source-files emptyDir: {} {{- else }} diff --git a/helm/values.yaml b/helm/values.yaml index 90479ece2..4feb57d13 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -131,49 +131,49 @@ ingress: ## needed here, unless you wish to deviate from the official metacatui release version defined in ## the helm chart (see Chart.yaml) ## -source: - ## @param source.from Options are "package" (the default), "git", or "pvc" - ## * "package" will download a release package from the metacatui git repository, unzip it, and - ## install the files in local pod storage (emptyDir{}) - ## * "git" will clone a specific branch or tag from the metacatui git repository, and install the - ## files in local pod storage (emptyDir{}) - ## * "pvc" expects to find a pre-configured PVC containing the files to be used. Note you will - ## need to provide values for a pre-configured PVC in 'volumes', below - ## - from: package - - ## @param source.package (default): Download the official release version defined in Chart.yaml - ## Note these settings ignored unless 'source.from:' is set to 'package' - ## example: - ## source.from: package - ## source.package.location: "https://github.com/NCEAS/metacatui/archive" - ## source.package.version: "2.26.0" - ## ...will download: https://github.com/NCEAS/metacatui/archive/2.26.0.zip - ## - package: - ## @param source.package.location The remote location where the release zipfile is hosted - ## - location: "https://github.com/NCEAS/metacatui/archive" - - ## source.package.version override the release version defined in Chart.yaml - ## Assumes release is a zipfile named .zip - ## LEAVE COMMENTED UNLESS YOU NEED TO OVERRIDE THE CHART SETTING! - # version: "2.29.1" - - ## @param source.git clone a specific branch or tag from the metacatui git repository, and install - ## the files in local pod storage (emptyDir{}) - ## Note these settings ignored unless 'source.from:' is set to 'git' - ## - git: - ## @param source.git.repoUrl the https url of the repo to be cloned - repoUrl: "https://github.com/NCEAS/metacatui.git" - - ## @param source.git.revision Any string that makes sense after the command `git checkout`... - ## - for example: - ## revision: "tags/2.29.0" => git checkout tags/2.29.0 - ## revision: "develop" => git checkout develop - ## - revision: "develop" +#source: +# ## @param source.from Options are "package" (the default), "git", or "pvc" +# ## * "package" will download a release package from the metacatui git repository, unzip it, and +# ## install the files in local pod storage (emptyDir{}) +# ## * "git" will clone a specific branch or tag from the metacatui git repository, and install the +# ## files in local pod storage (emptyDir{}) +# ## * "pvc" expects to find a pre-configured PVC containing the files to be used. Note you will +# ## need to provide values for a pre-configured PVC in 'volumes', below +# ## +# from: package +# +# ## @param source.package (default): Download the official release version defined in Chart.yaml +# ## Note these settings ignored unless 'source.from:' is set to 'package' +# ## example: +# ## source.from: package +# ## source.package.location: "https://github.com/NCEAS/metacatui/archive" +# ## source.package.version: "2.26.0" +# ## ...will download: https://github.com/NCEAS/metacatui/archive/2.26.0.zip +# ## +# package: +# ## @param source.package.location The remote location where the release zipfile is hosted +# ## +# location: "https://github.com/NCEAS/metacatui/archive" +# +# ## source.package.version override the release version defined in Chart.yaml +# ## Assumes release is a zipfile named .zip +# ## LEAVE COMMENTED UNLESS YOU NEED TO OVERRIDE THE CHART SETTING! +# # version: "2.29.1" +# +# ## @param source.git clone a specific branch or tag from the metacatui git repository, and install +# ## the files in local pod storage (emptyDir{}) +# ## Note these settings ignored unless 'source.from:' is set to 'git' +# ## +# git: +# ## @param source.git.repoUrl the https url of the repo to be cloned +# repoUrl: "https://github.com/NCEAS/metacatui.git" +# +# ## @param source.git.revision Any string that makes sense after the command `git checkout`... +# ## - for example: +# ## revision: "tags/2.29.0" => git checkout tags/2.29.0 +# ## revision: "develop" => git checkout develop +# ## +# revision: "develop" ## volumes: Uncomment and provide values if you want to use a pre-configured PVC instead of doing a ## git checkout From 93fc1423ef816b411192b7469f16f15f7c00d18d Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Wed, 19 Jun 2024 16:15:55 -0700 Subject: [PATCH 031/169] rename for clarity --- helm/admin/{metacatui-pv.yaml => metacatui-sourcecode-pv.yaml} | 0 helm/admin/{metacatui-pvc.yaml => metacatui-sourcecode-pvc.yaml} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename helm/admin/{metacatui-pv.yaml => metacatui-sourcecode-pv.yaml} (100%) rename helm/admin/{metacatui-pvc.yaml => metacatui-sourcecode-pvc.yaml} (100%) diff --git a/helm/admin/metacatui-pv.yaml b/helm/admin/metacatui-sourcecode-pv.yaml similarity index 100% rename from helm/admin/metacatui-pv.yaml rename to helm/admin/metacatui-sourcecode-pv.yaml diff --git a/helm/admin/metacatui-pvc.yaml b/helm/admin/metacatui-sourcecode-pvc.yaml similarity index 100% rename from helm/admin/metacatui-pvc.yaml rename to helm/admin/metacatui-sourcecode-pvc.yaml From da202304dc7d9403edb59f0e35e5b792bb81e4f5 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Wed, 19 Jun 2024 18:55:27 -0700 Subject: [PATCH 032/169] enable pvc for custom theme --- helm/README.md | 111 +++++++++++++++++------ helm/config/config.js | 4 +- helm/templates/deployment.yaml | 36 +++++--- helm/values.yaml | 160 +++++++++++++++++++++------------ 4 files changed, 215 insertions(+), 96 deletions(-) diff --git a/helm/README.md b/helm/README.md index 3c3fb2df0..4c7aa125c 100644 --- a/helm/README.md +++ b/helm/README.md @@ -1,43 +1,102 @@ # MetacatUI Helm chart -This is a simple helm chart for debugging a MetacatUI deployment. +This is a helm chart for deploying MetacatUI. -## Steps to get started for deployment in a Kubernetes cluster: +--- +## Deployment in a Kubernetes cluster: -1. modify values.yaml as appropriate +1. Modify values.yaml as appropriate 2. install the helm chart: + ```shell + $ helm -n knb upgrade --install knbmcui ./helm + ``` -```shell -$ helm -n knb upgrade --install knbmcui ./helm -``` -There's no need to set up any persistent storage - the chart will automatically check out the -metacatui static content from GitHub, and install it on an "emptyDir" that is automatically -created for you. +There's no need to set up any persistent storage, unless you wish to add your own theme. The chart +ships with [a few pre-defined themes](https://github.com/NCEAS/metacatui/tree/main/src/js/themes), +which can be selected in values.yaml. +--- +## MetacatUI Configuration Files ('config.js') -## Steps to get started for development on localhost (e.g. Rancher Desktop/Docker Desktop): +The k8s version of Metacatui requires two 'config.js' configuration files: +1. the "root config" at the path `{root}/config/config.js`, and +2. the "theme config" in the theme directory itself (for example, see the knb `config.js` at: + https://github.com/NCEAS/metacatui/blob/main/src/js/themes/knb/config.js) -0. Create a namespace `mcui` for the deployment (or pick another of your liking) -1. Create a PV that is mapped to a local `hostPath` directory that contains the web files to deploy -2. Create a PVC for the PV -3. Create a nginx deployment behind an ingress that mounts the PVC where nginx expects its web files to live +The "root config" file must, at an absolute minimum, contain the name of the theme to be used; e.g: +```javascript + MetacatUI.AppConfig = { + theme: "knb" + }; +``` +...and metacatui will then load that theme and the corresponding "theme config". -To deploy this, you need to 1) create the PV and PVC for your system layout, 2) modify the values.yaml to your hostname for the Ingress definition, and 3) install the helm chart: +If any additional settings are defined in the "root config", Metacatui will use them to override +corresponding settings in the "theme config". +By default, this chart creates a simple "root config", which will contain any values of the form: +`key: stringValue,` or `key: intValue,` that are provided in the `appConfig:` section of +[values.yaml](./values.yaml). + +If you need to provide more-complex overrides, set `appConfig.enabled: false`, and manually +create your own configMap named `-metacatui-config-js`, containing your custom +config.js: ```shell -$ helm -n mcui upgrade --install --debug mcui ./helm -Release "mcui" has been upgraded. Happy Helming! -NAME: mcui -LAST DEPLOYED: Wed Apr 17 19:45:58 2024 -NAMESPACE: mcui -STATUS: deployed -REVISION: 11 -NOTES: -1. Get the application URL by running these commands: - http://firn.local/ +kubectl create configmap -metacatui-config-js \ + --from-file=config.js= ``` +--- + +## Using a Custom Theme + +See [the MetacatUI +documentation](https://nceas.github.io/metacatui/install/configuration/index.html) for help with +creating custom themes. + +If you wish to deploy MetacatUI with your own custom theme, instead of using one of the themes that +are bundled with metacatui, you will need to provide the files for that theme (including its +"theme config" file) on shared filesystem, accessed via a manually-created Persistent Volume (PV) +mount and a Persistent Volume Claim (PVC). Example files for creating PVs and PVCs are provided +in the [admin](./admin) directory. + +Once you've got the chart deployed (see above), next steps are: +1. Copy your theme files to a directory on a filesystem that is accessible from your Kubernetes + cluster +2. Create a Persistent Volume (PV) pointing to the correct directory on the filesystem +3. Create a PVC for the PV, and edit the `customTheme:` section in values.yaml +4. upgrade the helm chart + ```shell + $ helm -n knb upgrade --install knbmcui ./helm + ``` + +--- + +## Development on Localhost +(e.g. Rancher Desktop/Docker Desktop) + +1. Create a namespace for the deployment (e.g. `mcui`) +2. Create a PV that is mapped to a local `hostPath` directory containing the source code +3. Create a PVC for the PV +4. Modify values.yaml: + 1. Add the name of the PVC, so MetacatUI can find the files + 2. Set your hostname for the Ingress definition +5. install the helm chart: + ```shell + $ helm -n mcui upgrade --install --debug mcui ./helm + Release "mcui" has been upgraded. Happy Helming! + NAME: mcui + LAST DEPLOYED: Wed Apr 17 19:45:58 2024 + NAMESPACE: mcui + STATUS: deployed + REVISION: 11 + ...etc + ``` + +You can now edit the MetacatUI source files, and changes will be immediately visible in your k8s +deployment. -You can now edit the MetacatUI files and changes will be immediately visible in your chart. You will likely need to edit the `config.js` file to get a minimal setup working. For example, I used the `config/config.js` with these contents: +You will likely need to edit the `config.js` file to get a minimal setup working. Example contents +for `config/config.js`: ```javascript MetacatUI.AppConfig = { diff --git a/helm/config/config.js b/helm/config/config.js index a3f86477c..dc5e7db48 100644 --- a/helm/config/config.js +++ b/helm/config/config.js @@ -1,5 +1,5 @@ MetacatUI.AppConfig = { - {{- $ignoreList := list "enabled" "root" "baseUrl" "metacatContext" "d1CNBaseUrl" -}} + {{- $ignoreList := list "enabled" "theme" "root" "baseUrl" "metacatContext" "d1CNBaseUrl" -}} {{- range $key, $value := .Values.appConfig }} {{- if not (has $key $ignoreList) }} {{- if eq (typeOf $value) "string" }} @@ -10,7 +10,7 @@ MetacatUI.AppConfig = { {{- end }} {{- end -}} {{- include "metacatui.cn.url" . | nindent 4 }} - theme: {{ required "theme_is_REQUIRED" .Values.appConfig.theme | quote }}, + theme: {{ required "metacatUiThemeName_is_REQUIRED" .Values.global.metacatUiThemeName | quote }}, root: {{ required "root_is_REQUIRED" .Values.appConfig.root | quote }}, metacatContext: {{ required "metacatAppContext_is_REQUIRED" .Values.global.metacatAppContext | quote }}, baseUrl: {{ required "metacatExternalBaseUrl_is_REQUIRED" .Values.global.metacatExternalBaseUrl | quote }} diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index d1ce2edd0..69b4e0a2f 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -99,7 +99,7 @@ spec: {{- end }} {{- else }} httpGet: - path: {{ .Values.appConfig.root }} + path: {{ .Values.global.metacatUiWebRoot }} port: http {{- end }} {{- end }} @@ -111,34 +111,48 @@ spec: {{- end }} {{- else }} httpGet: - path: {{ .Values.appConfig.root }} + path: {{ .Values.global.metacatUiWebRoot }} port: http {{- end }} {{- end }} resources: {{- toYaml .Values.resources | nindent 12 }} volumeMounts: + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} - name: {{ .Release.Name }}-mcui-source-files mountPath: /usr/share/nginx/html + {{- if .Values.customTheme.enabled }} + - name: {{ .Release.Name }}-mcui-custom-theme-files + mountPath: {{ include "metacatui.root.mountpath" . }}/js/themes/{{ .Values.global.metacatUiThemeName }} + {{- if .Values.customTheme.subPath }} + subPath: {{ .Values.customTheme.subPath }} + {{- end }} + {{- end }} - name: {{ .Release.Name }}-mcui-config-js mountPath: {{ include "metacatui.root.mountpath" . }}/config/config.js subPath: config.js - {{- with .Values.volumeMounts }} - {{- toYaml . | nindent 12 }} - {{- end }} volumes: - name: {{ .Release.Name }}-mcui-config-js configMap: name: {{ .Release.Name }}-metacatui-config-js defaultMode: 0644 - {{- if ne $source "pvc" }} - name: {{ .Release.Name }}-mcui-source-files + {{- if eq $source "pvc" }} + persistentVolumeClaim: + claimName: {{ required "claimName_REQUIRED" .Values.source.pvc.sourceCodeClaimName }} + {{- else }} emptyDir: {} - {{- else }} - {{- with .Values.volumes }} - {{- toYaml . | nindent 8 }} - {{- end }} - {{- end }} + {{- end }} + {{- if .Values.customTheme.enabled }} + - name: {{ .Release.Name }}-mcui-custom-theme-files + persistentVolumeClaim: + claimName: {{ required "claimName_REQUIRED" .Values.customTheme.claimName }} + {{- end }} + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/helm/values.yaml b/helm/values.yaml index 4feb57d13..f158a365a 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -6,6 +6,15 @@ ## global: + ## @param global.metacatUiThemeName The theme name to use. Required, even if overriding config.js + ## + metacatUiThemeName: "knb" + + ## @param global.metacatUiWebRoot The url root to be appended after the metacatui baseUrl. + ## Starts with "/". Required, even if overriding config.js + ## + metacatUiWebRoot: "/" + ## @param global.metacatExternalBaseUrl metacat base url, accessible from outside the cluster. ## Include protocol and trailing slash, but not the context; e.g.: "https://test.arcticdata.io/" ## @@ -64,6 +73,8 @@ global: ## ## * * * IMPORTANT NOTE: * * * DO NOT SET THE FOLLOWING VALUES IN THIS SECTION! * * * ## They will be ignored here, because they are populated from the "global" section, above: +## * theme: uses '.Values.global.metacatUiThemeName' +## * root: uses '.Values.global.metacatUiWebRoot' ## * baseUrl: uses '.Values.global.metacatExternalBaseUrl' ## * d1CNBaseUrl: uses base URL portion of '.Values.global.d1ClientCnUrl' ## * metacatContext: uses '.Values.global.metacatAppContext' @@ -74,15 +85,49 @@ appConfig: ## create your own configMap named 'YourReleaseName-metacatui-config-js', containing your custom ## config.js. ## + ## IMPORTANT: global.metacatUiThemeName MUST be set to your theme name, even if you are disabling + ## appConfig and providing a custom config.js in a configMap. + ## enabled: true - ## @param appConfig.root The url root to be appended after the metacatui baseUrl. Starts with "/" +## @param themeFilesClaimName Provide source files for a custom Theme (also see 'appConfig.theme'). +## NOTE that global.metacatUiThemeName MUST match the theme of your custom theme +## +customTheme: + ## @param customTheme.enabled Provide custom Theme files on a pre-configured PVC ## - root: "/" + enabled: true - ## @param appConfig.theme Corresponds to the name of a directory in src/js/themes/. + ## @param customTheme.themeClaimName substitute your own PVC name ## - theme: "knb" + claimName: "Your-Pre-Configured-pvc-name" + + ## @param customTheme.subPath path to theme directory, within mounted filesystem + ## For example, if you cloned https://github.com/NCEAS/metacatui-themes to the root of your + ## shared drive, it would look like this: + ## + ## /metacatui-themes + ## └── src + ## ├── cerp + ## │ └── js + ## │ └── themes + ## │ └── cerp + ## │ ├── config.js + ## │ ├── css + ## │ ├── ...etc + ## ├── drp + ## │ └── js + ## │ └── themes + ## │ └── drp + ## │ ├── config.js + ## │ ├── css + ## ...etc ├── ...etc + ## + ## ...so to use the drp theme, you would set the subPath to: + ## subPath: "metacatui-themes/src/drp/js/themes/drp" + ## + subPath: "" + ## @param livenessProbeEnabled Enable livenessProbe. Autoconfigured using .Values.appConfig.root ## To override autoconfig, keep 'livenessProbeEnabled: true', and define a livenessProbe; e.g: @@ -131,59 +176,60 @@ ingress: ## needed here, unless you wish to deviate from the official metacatui release version defined in ## the helm chart (see Chart.yaml) ## -#source: -# ## @param source.from Options are "package" (the default), "git", or "pvc" -# ## * "package" will download a release package from the metacatui git repository, unzip it, and -# ## install the files in local pod storage (emptyDir{}) -# ## * "git" will clone a specific branch or tag from the metacatui git repository, and install the -# ## files in local pod storage (emptyDir{}) -# ## * "pvc" expects to find a pre-configured PVC containing the files to be used. Note you will -# ## need to provide values for a pre-configured PVC in 'volumes', below -# ## -# from: package -# -# ## @param source.package (default): Download the official release version defined in Chart.yaml -# ## Note these settings ignored unless 'source.from:' is set to 'package' -# ## example: -# ## source.from: package -# ## source.package.location: "https://github.com/NCEAS/metacatui/archive" -# ## source.package.version: "2.26.0" -# ## ...will download: https://github.com/NCEAS/metacatui/archive/2.26.0.zip -# ## -# package: -# ## @param source.package.location The remote location where the release zipfile is hosted -# ## -# location: "https://github.com/NCEAS/metacatui/archive" -# -# ## source.package.version override the release version defined in Chart.yaml -# ## Assumes release is a zipfile named .zip -# ## LEAVE COMMENTED UNLESS YOU NEED TO OVERRIDE THE CHART SETTING! -# # version: "2.29.1" -# -# ## @param source.git clone a specific branch or tag from the metacatui git repository, and install -# ## the files in local pod storage (emptyDir{}) -# ## Note these settings ignored unless 'source.from:' is set to 'git' -# ## -# git: -# ## @param source.git.repoUrl the https url of the repo to be cloned -# repoUrl: "https://github.com/NCEAS/metacatui.git" -# -# ## @param source.git.revision Any string that makes sense after the command `git checkout`... -# ## - for example: -# ## revision: "tags/2.29.0" => git checkout tags/2.29.0 -# ## revision: "develop" => git checkout develop -# ## -# revision: "develop" - -## volumes: Uncomment and provide values if you want to use a pre-configured PVC instead of doing a -## git checkout -## Note these settings ignored unless 'source.from:' is set to 'pvc' -## -#volumes: -# ## volumes.name substitute your own release name, but do NOT change the '-mcui-source-files' part -# - name: -mcui-source-files -# persistentVolumeClaim: -# claimName: +source: + ## @param source.from Options are "package" (the default), "git", or "pvc" + ## * "package" will download a release package from the metacatui git repository, unzip it, and + ## install the files in local pod storage (emptyDir{}) + ## * "git" will clone a specific branch or tag from the metacatui git repository, and install the + ## files in local pod storage (emptyDir{}) + ## * "pvc" expects to find a pre-configured PVC containing the files to be used. Note you will + ## need to provide values for a pre-configured PVC in 'volumes', below + ## + from: package + + ## @param source.package (default): Download the official release version defined in Chart.yaml + ## example: + ## source.from: package + ## source.package.location: "https://github.com/NCEAS/metacatui/archive" + ## source.package.version: "2.26.0" + ## ...will download: https://github.com/NCEAS/metacatui/archive/2.26.0.zip + ## + ## Note these settings ignored unless 'source.from:' is set to 'package' + ## + package: + ## @param source.package.location The remote location where the release zipfile is hosted + ## + location: "https://github.com/NCEAS/metacatui/archive" + + ## source.package.version override the release version defined in Chart.yaml + ## Assumes release is a zipfile named .zip + ## LEAVE UNSET, UNLESS YOU NEED TO OVERRIDE THE CHART SETTING! + version: "" + + ## @param source.git clone a specific branch or tag from the metacatui git repository, and install + ## the files in local pod storage (emptyDir{}) + ## + ## Note these settings ignored unless 'source.from:' is set to 'git' + ## + git: + ## @param source.git.repoUrl the https url of the repo to be cloned + repoUrl: "https://github.com/NCEAS/metacatui.git" + + ## @param source.git.revision Any string that makes sense after the command `git checkout`... + ## - for example: + ## revision: "tags/2.29.0" => git checkout tags/2.29.0 + ## revision: "develop" => git checkout develop + ## + revision: "develop" + + ## @param source.pvc Provide custom source files on a pre-configured PVC (typ. for development) + ## + ## Note these settings ignored unless 'source.from:' is set to 'pvc' + ## + pvc: + ## @param volumes.name substitute your own PVC name + ## + sourceCodeClaimName: "Your-Pre-Configured-pvc-name" replicaCount: 1 From cb1f3e2c4a01c181c457a5ef6c539a9e1a462503 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Wed, 19 Jun 2024 19:00:31 -0700 Subject: [PATCH 033/169] example pv/pvc files --- helm/admin/metacatui-customtheme-pv.yaml | 30 +++++++++++++++++++++++ helm/admin/metacatui-customtheme-pvc.yaml | 20 +++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 helm/admin/metacatui-customtheme-pv.yaml create mode 100644 helm/admin/metacatui-customtheme-pvc.yaml diff --git a/helm/admin/metacatui-customtheme-pv.yaml b/helm/admin/metacatui-customtheme-pv.yaml new file mode 100644 index 000000000..77abf14d2 --- /dev/null +++ b/helm/admin/metacatui-customtheme-pv.yaml @@ -0,0 +1,30 @@ +## EXAMPLE file for manually creating a Persistent Volume to store a metacatui custom theme. +## Needed only if 'customTheme.enabled:' is set to 'true' in values.yaml. +## EDIT this file to replace "$RELEASENAME", "$YOUR-CLUSTER-ID" and the "rootPath" +apiVersion: v1 +kind: PersistentVolume +metadata: + # See https://github.com/DataONEorg/k8s-cluster/blob/main/storage/storage.md#dataone-volume-naming-conventions + # cephs-{release}-{function}-{instance}, where {release} usually = {namespace} + name: &pv-name cephfs-$RELEASENAME-metacatui-customtheme +spec: + accessModes: + - ReadOnlyMany + capacity: + storage: 100Mi + csi: + driver: cephfs.csi.ceph.com + nodeStageSecretRef: + # node stage secret name + name: csi-cephfs-$RELEASENAME-secret + # node stage secret namespace where above secret is created + namespace: ceph-csi-cephfs + volumeAttributes: + clusterID: $YOUR-CLUSTER-ID + fsName: cephfs + rootPath: /volumes/YOUR-subvol-group/YOUR-subvol/YOUR-ID/repos/YOUR_REPO/metacatui + staticVolume: "true" + volumeHandle: *pv-name + persistentVolumeReclaimPolicy: Retain + storageClassName: csi-cephfs-sc + volumeMode: Filesystem diff --git a/helm/admin/metacatui-customtheme-pvc.yaml b/helm/admin/metacatui-customtheme-pvc.yaml new file mode 100644 index 000000000..16c30ca4b --- /dev/null +++ b/helm/admin/metacatui-customtheme-pvc.yaml @@ -0,0 +1,20 @@ +## EXAMPLE file for manually creating a Persistent Volume Claim to access the PV containing a +## metacatui custom theme. +## Needed only if 'customTheme.enabled:' is set to 'true' in values.yaml. +## EDIT this file to replace "$RELEASENAME" and "$NAMESPACE" +## +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: $RELEASENAME-metacatui-customtheme + ## NOTE: namespace must match the deployment namespace + namespace: $NAMESPACE +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Mi + storageClassName: csi-cephfs-sc + volumeMode: Filesystem + volumeName: cephfs-RELEASENAME-metacatui-customtheme From d46df5914d783e3b6e3e23d32a12eb0efd258319 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 1 Jul 2024 11:56:32 -0700 Subject: [PATCH 034/169] update app version --- helm/Chart.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 7ead3d7aa..54f9fdf34 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -15,10 +15,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.5.0 +version: 0.6.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "2.29.1" +appVersion: "2.30.0" From 4049866cbaa297f4ec3f8c77a7dbf68306b61567 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 1 Jul 2024 16:40:52 -0700 Subject: [PATCH 035/169] move root to global --- helm/config/config.js | 2 +- helm/templates/_helpers.tpl | 2 +- helm/values.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/helm/config/config.js b/helm/config/config.js index dc5e7db48..fff3cae0d 100644 --- a/helm/config/config.js +++ b/helm/config/config.js @@ -11,7 +11,7 @@ MetacatUI.AppConfig = { {{- end -}} {{- include "metacatui.cn.url" . | nindent 4 }} theme: {{ required "metacatUiThemeName_is_REQUIRED" .Values.global.metacatUiThemeName | quote }}, - root: {{ required "root_is_REQUIRED" .Values.appConfig.root | quote }}, + root: {{ required "root_is_REQUIRED" .Values.global.metacatUiWebRoot | quote }}, metacatContext: {{ required "metacatAppContext_is_REQUIRED" .Values.global.metacatAppContext | quote }}, baseUrl: {{ required "metacatExternalBaseUrl_is_REQUIRED" .Values.global.metacatExternalBaseUrl | quote }} } diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index 09204bdf9..da142bcc9 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -83,7 +83,7 @@ d1CNBaseUrl: "{{ $baseCnURL }}", Remove trailing slash from root, if it exists */}} {{- define "metacatui.clean.root" -}} -{{- $cleanedRoot := regexReplaceAll "/$" .Values.appConfig.root "" -}} +{{- $cleanedRoot := regexReplaceAll "/$" .Values.global.metacatUiWebRoot "" -}} {{- $cleanedRoot }} {{- end }} diff --git a/helm/values.yaml b/helm/values.yaml index f158a365a..f57b90f90 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -96,7 +96,7 @@ appConfig: customTheme: ## @param customTheme.enabled Provide custom Theme files on a pre-configured PVC ## - enabled: true + enabled: false ## @param customTheme.themeClaimName substitute your own PVC name ## From b7f1b4fccbfde799df12b1e15237f8b6a3325f6f Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:20:23 -0700 Subject: [PATCH 036/169] Update helm/README.md Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- helm/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/helm/README.md b/helm/README.md index 4c7aa125c..3341133c4 100644 --- a/helm/README.md +++ b/helm/README.md @@ -60,6 +60,7 @@ mount and a Persistent Volume Claim (PVC). Example files for creating PVs and PV in the [admin](./admin) directory. Once you've got the chart deployed (see above), next steps are: + 1. Copy your theme files to a directory on a filesystem that is accessible from your Kubernetes cluster 2. Create a Persistent Volume (PV) pointing to the correct directory on the filesystem From 3d7a4e021dda9d882673203fe6a967da9ccd9365 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:21:03 -0700 Subject: [PATCH 037/169] Update helm/README.md Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- helm/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/helm/README.md b/helm/README.md index 3341133c4..1308796fe 100644 --- a/helm/README.md +++ b/helm/README.md @@ -66,9 +66,8 @@ Once you've got the chart deployed (see above), next steps are: 2. Create a Persistent Volume (PV) pointing to the correct directory on the filesystem 3. Create a PVC for the PV, and edit the `customTheme:` section in values.yaml 4. upgrade the helm chart - ```shell - $ helm -n knb upgrade --install knbmcui ./helm - ``` + ```shell + $ helm -n knb upgrade --install knbmcui ./helm --- From e26dcef253ba6f57af5aba0764ddc1ec316de1b8 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:21:36 -0700 Subject: [PATCH 038/169] Update helm/README.md Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- helm/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/helm/README.md b/helm/README.md index 1308796fe..c4766f66e 100644 --- a/helm/README.md +++ b/helm/README.md @@ -72,6 +72,7 @@ Once you've got the chart deployed (see above), next steps are: --- ## Development on Localhost + (e.g. Rancher Desktop/Docker Desktop) 1. Create a namespace for the deployment (e.g. `mcui`) From 9e9d3f78dc646bb97ec0c3ee50445c3d87607a7e Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:21:59 -0700 Subject: [PATCH 039/169] Update helm/README.md Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- helm/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/helm/README.md b/helm/README.md index c4766f66e..44cba8faf 100644 --- a/helm/README.md +++ b/helm/README.md @@ -24,6 +24,7 @@ The k8s version of Metacatui requires two 'config.js' configuration files: https://github.com/NCEAS/metacatui/blob/main/src/js/themes/knb/config.js) The "root config" file must, at an absolute minimum, contain the name of the theme to be used; e.g: + ```javascript MetacatUI.AppConfig = { theme: "knb" From 65183d0498d019e34d2069956bc9540115533168 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:22:15 -0700 Subject: [PATCH 040/169] Update helm/README.md Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- helm/README.md | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/helm/README.md b/helm/README.md index 44cba8faf..256a31d5e 100644 --- a/helm/README.md +++ b/helm/README.md @@ -83,16 +83,15 @@ Once you've got the chart deployed (see above), next steps are: 1. Add the name of the PVC, so MetacatUI can find the files 2. Set your hostname for the Ingress definition 5. install the helm chart: - ```shell - $ helm -n mcui upgrade --install --debug mcui ./helm - Release "mcui" has been upgraded. Happy Helming! - NAME: mcui - LAST DEPLOYED: Wed Apr 17 19:45:58 2024 - NAMESPACE: mcui - STATUS: deployed - REVISION: 11 - ...etc - ``` + ```shell + $ helm -n mcui upgrade --install --debug mcui ./helm + Release "mcui" has been upgraded. Happy Helming! + NAME: mcui + LAST DEPLOYED: Wed Apr 17 19:45:58 2024 + NAMESPACE: mcui + STATUS: deployed + REVISION: 11 + ...etc You can now edit the MetacatUI source files, and changes will be immediately visible in your k8s deployment. From 60c34780c9a373142762ffa2001cd445432b878b Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:22:38 -0700 Subject: [PATCH 041/169] Update helm/admin/metacatui-customtheme-pvc.yaml Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- helm/admin/metacatui-customtheme-pvc.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/admin/metacatui-customtheme-pvc.yaml b/helm/admin/metacatui-customtheme-pvc.yaml index 16c30ca4b..df6ea6303 100644 --- a/helm/admin/metacatui-customtheme-pvc.yaml +++ b/helm/admin/metacatui-customtheme-pvc.yaml @@ -6,7 +6,7 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: - name: $RELEASENAME-metacatui-customtheme + name: $RELEASENAME-metacatui-customtheme ## NOTE: namespace must match the deployment namespace namespace: $NAMESPACE spec: From 817a12eea8ed3b4913a57515292b4cb5b5dfae73 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:22:58 -0700 Subject: [PATCH 042/169] Update helm/values.yaml Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- helm/values.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/helm/values.yaml b/helm/values.yaml index f57b90f90..8d17f1c38 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -128,7 +128,6 @@ customTheme: ## subPath: "" - ## @param livenessProbeEnabled Enable livenessProbe. Autoconfigured using .Values.appConfig.root ## To override autoconfig, keep 'livenessProbeEnabled: true', and define a livenessProbe; e.g: ## livenessProbe: From 5e66f5fe07a9de2c6882235dd8b75719f517e74f Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:23:37 -0700 Subject: [PATCH 043/169] Update helm/admin/metacatui-customtheme-pvc.yaml Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- helm/admin/metacatui-customtheme-pvc.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/admin/metacatui-customtheme-pvc.yaml b/helm/admin/metacatui-customtheme-pvc.yaml index df6ea6303..67a3c6048 100644 --- a/helm/admin/metacatui-customtheme-pvc.yaml +++ b/helm/admin/metacatui-customtheme-pvc.yaml @@ -11,7 +11,7 @@ metadata: namespace: $NAMESPACE spec: accessModes: - - ReadWriteOnce + - ReadWriteOnce resources: requests: storage: 100Mi From c998b374d5512c71b917ec691004f88d3ca8b586 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:24:09 -0700 Subject: [PATCH 044/169] Update helm/README.md Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- helm/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helm/README.md b/helm/README.md index 256a31d5e..1eb77d5c9 100644 --- a/helm/README.md +++ b/helm/README.md @@ -26,9 +26,9 @@ The k8s version of Metacatui requires two 'config.js' configuration files: The "root config" file must, at an absolute minimum, contain the name of the theme to be used; e.g: ```javascript - MetacatUI.AppConfig = { - theme: "knb" - }; +MetacatUI.AppConfig = { + theme: "knb", +}; ``` ...and metacatui will then load that theme and the corresponding "theme config". From 2b6f427975743228ed10e6004f5a04c33708315d Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:24:36 -0700 Subject: [PATCH 045/169] Update helm/README.md Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- helm/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/helm/README.md b/helm/README.md index 1eb77d5c9..eea2e6c9a 100644 --- a/helm/README.md +++ b/helm/README.md @@ -3,6 +3,7 @@ This is a helm chart for deploying MetacatUI. --- + ## Deployment in a Kubernetes cluster: 1. Modify values.yaml as appropriate From becc7dfbcb45408dab6cbeef9f592e789cab7617 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:25:31 -0700 Subject: [PATCH 046/169] Update helm/README.md Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- helm/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helm/README.md b/helm/README.md index eea2e6c9a..36e0f645c 100644 --- a/helm/README.md +++ b/helm/README.md @@ -102,7 +102,7 @@ for `config/config.js`: ```javascript MetacatUI.AppConfig = { - root: "/", - baseUrl: "https://dev.nceas.ucsb.edu/knb/d1/mn" -} + root: "/", + baseUrl: "https://dev.nceas.ucsb.edu/knb/d1/mn", +}; ``` From bfaf760174df0475781a328a9d2a3549f2ac4362 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:26:08 -0700 Subject: [PATCH 047/169] Update helm/README.md Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- helm/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/helm/README.md b/helm/README.md index 36e0f645c..4afc112a3 100644 --- a/helm/README.md +++ b/helm/README.md @@ -31,6 +31,7 @@ MetacatUI.AppConfig = { theme: "knb", }; ``` + ...and metacatui will then load that theme and the corresponding "theme config". If any additional settings are defined in the "root config", Metacatui will use them to override From 9304885b0be9c261ff7fd84b507216c7f300fcd1 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:26:31 -0700 Subject: [PATCH 048/169] Update helm/README.md Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- helm/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/helm/README.md b/helm/README.md index 4afc112a3..b7dac3415 100644 --- a/helm/README.md +++ b/helm/README.md @@ -8,9 +8,8 @@ This is a helm chart for deploying MetacatUI. 1. Modify values.yaml as appropriate 2. install the helm chart: - ```shell - $ helm -n knb upgrade --install knbmcui ./helm - ``` + ```shell + $ helm -n knb upgrade --install knbmcui ./helm There's no need to set up any persistent storage, unless you wish to add your own theme. The chart ships with [a few pre-defined themes](https://github.com/NCEAS/metacatui/tree/main/src/js/themes), From 588f9a3578ed74ea109610b98708ae5923c41c15 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:26:56 -0700 Subject: [PATCH 049/169] Update helm/README.md Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- helm/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/helm/README.md b/helm/README.md index b7dac3415..76e9f69b0 100644 --- a/helm/README.md +++ b/helm/README.md @@ -16,6 +16,7 @@ ships with [a few pre-defined themes](https://github.com/NCEAS/metacatui/tree/ma which can be selected in values.yaml. --- + ## MetacatUI Configuration Files ('config.js') The k8s version of Metacatui requires two 'config.js' configuration files: From 991c99938d8565ca2bf16064815eb2d382467824 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:27:22 -0700 Subject: [PATCH 050/169] Update helm/README.md Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- helm/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/README.md b/helm/README.md index 76e9f69b0..8626fc595 100644 --- a/helm/README.md +++ b/helm/README.md @@ -22,7 +22,7 @@ which can be selected in values.yaml. The k8s version of Metacatui requires two 'config.js' configuration files: 1. the "root config" at the path `{root}/config/config.js`, and 2. the "theme config" in the theme directory itself (for example, see the knb `config.js` at: - https://github.com/NCEAS/metacatui/blob/main/src/js/themes/knb/config.js) + https://github.com/NCEAS/metacatui/blob/main/src/js/themes/knb/config.js) The "root config" file must, at an absolute minimum, contain the name of the theme to be used; e.g: From 1101da0c2d4dec539ee4b2a297e60d63549be893 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:27:56 -0700 Subject: [PATCH 051/169] Update helm/values.yaml Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- helm/values.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/helm/values.yaml b/helm/values.yaml index 8d17f1c38..eaaa4246a 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -34,7 +34,6 @@ global: ## d1ClientCnUrl: "https://cn.dataone.org/cn" - ## @section MetacatUI Configuration Files ('config.js') ## ## The k8s version of Metacatui requires two 'config.js' configuration files: From 5ea7d7605e88343c1883f54db68002455a225480 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:28:38 -0700 Subject: [PATCH 052/169] Update helm/README.md Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- helm/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/helm/README.md b/helm/README.md index 8626fc595..9385d903b 100644 --- a/helm/README.md +++ b/helm/README.md @@ -44,6 +44,7 @@ By default, this chart creates a simple "root config", which will contain any va If you need to provide more-complex overrides, set `appConfig.enabled: false`, and manually create your own configMap named `-metacatui-config-js`, containing your custom config.js: + ```shell kubectl create configmap -metacatui-config-js \ --from-file=config.js= From 2b95d2558f4ef11ff5ffa2c104e59b94e311e490 Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:29:08 -0700 Subject: [PATCH 053/169] Update helm/README.md Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- helm/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/helm/README.md b/helm/README.md index 9385d903b..b3b1d3ec8 100644 --- a/helm/README.md +++ b/helm/README.md @@ -49,6 +49,7 @@ config.js: kubectl create configmap -metacatui-config-js \ --from-file=config.js= ``` + --- ## Using a Custom Theme From 44b7a9328e058550e5479c7bfd512042b3ef1a9e Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:29:51 -0700 Subject: [PATCH 054/169] Update helm/README.md Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- helm/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/helm/README.md b/helm/README.md index b3b1d3ec8..6fce58401 100644 --- a/helm/README.md +++ b/helm/README.md @@ -20,6 +20,7 @@ which can be selected in values.yaml. ## MetacatUI Configuration Files ('config.js') The k8s version of Metacatui requires two 'config.js' configuration files: + 1. the "root config" at the path `{root}/config/config.js`, and 2. the "theme config" in the theme directory itself (for example, see the knb `config.js` at: https://github.com/NCEAS/metacatui/blob/main/src/js/themes/knb/config.js) From 0d6ea3b96584b9970a75b8ba51c3fddaa7b5c38a Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:35:21 -0700 Subject: [PATCH 055/169] linting errors --- helm/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/helm/README.md b/helm/README.md index 6fce58401..a1d3443b5 100644 --- a/helm/README.md +++ b/helm/README.md @@ -8,8 +8,10 @@ This is a helm chart for deploying MetacatUI. 1. Modify values.yaml as appropriate 2. install the helm chart: + ```shell $ helm -n knb upgrade --install knbmcui ./helm + ``` There's no need to set up any persistent storage, unless you wish to add your own theme. The chart ships with [a few pre-defined themes](https://github.com/NCEAS/metacatui/tree/main/src/js/themes), @@ -72,8 +74,10 @@ Once you've got the chart deployed (see above), next steps are: 2. Create a Persistent Volume (PV) pointing to the correct directory on the filesystem 3. Create a PVC for the PV, and edit the `customTheme:` section in values.yaml 4. upgrade the helm chart + ```shell $ helm -n knb upgrade --install knbmcui ./helm + ``` --- @@ -88,6 +92,7 @@ Once you've got the chart deployed (see above), next steps are: 1. Add the name of the PVC, so MetacatUI can find the files 2. Set your hostname for the Ingress definition 5. install the helm chart: + ```shell $ helm -n mcui upgrade --install --debug mcui ./helm Release "mcui" has been upgraded. Happy Helming! @@ -97,7 +102,8 @@ Once you've got the chart deployed (see above), next steps are: STATUS: deployed REVISION: 11 ...etc - + ``` + You can now edit the MetacatUI source files, and changes will be immediately visible in your k8s deployment. From 7beedf334208412695f312849fa3ec4bb4b03830 Mon Sep 17 00:00:00 2001 From: robyngit Date: Tue, 2 Jul 2024 16:07:00 -0400 Subject: [PATCH 056/169] Syntax fix to commit d49b1471b01 --- eslint.config.mjs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index a02156544..8d6f81b4b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -32,12 +32,10 @@ const airbnbRulesOverrides = { // We are using RequireJS "import/no-unresolved": "off", // Allow unused variables if they start with an underscore - "variables/no-unused-vars": [ + "no-unused-vars": [ "error", { - vars: "all", - args: "after-used", - ignoreRestSiblings: true, + argsIgnorePattern: "^_", varsIgnorePattern: "^_", }, ], From eff556bc30a0061303334745b12c51d14d6a4480 Mon Sep 17 00:00:00 2001 From: robyngit Date: Tue, 2 Jul 2024 15:20:21 -0400 Subject: [PATCH 057/169] Fix linting errors in QueryFields collection Issue #2253 --- src/js/collections/queryFields/QueryFields.js | 149 +++++++----------- 1 file changed, 58 insertions(+), 91 deletions(-) diff --git a/src/js/collections/queryFields/QueryFields.js b/src/js/collections/queryFields/QueryFields.js index e1746692d..3efc6dfe8 100644 --- a/src/js/collections/queryFields/QueryFields.js +++ b/src/js/collections/queryFields/QueryFields.js @@ -1,10 +1,9 @@ -define([ - "jquery", - "underscore", - "backbone", - "x2js", - "models/queryFields/QueryField", -], function ($, _, Backbone, X2JS, QueryField) { +define(["underscore", "backbone", "x2js", "models/queryFields/QueryField"], ( + _, + Backbone, + X2JS, + QueryField, +) => { "use strict"; /** @@ -16,11 +15,11 @@ define([ * https://dataone-architecture-documentation.readthedocs.io/en/latest/design/SearchMetadata.html * @classcategory Collections/QueryFields * @name QueryFields - * @extends Backbone.Collection + * @augments Backbone.Collection * @since 2.14.0 - * @constructor + * @class */ - var QueryFields = Backbone.Collection.extend( + const QueryFields = Backbone.Collection.extend( /** @lends QueryFields.prototype */ { /** @@ -28,52 +27,31 @@ define([ */ model: QueryField, - /** - * initialize - Creates a new QueryFields collection - */ - initialize: function (models, options) { - try { - if (typeof options === "undefined") { - var options = {}; - } - } catch (e) { - console.log( - "Failed to initialize a Query Fields collection, error message: " + - e, - ); - } - }, - /** * comparator - A sortBy function that returns the order of each Query * Filter model based on its position in the categoriesMap object. - * * @param {QueryFilter} model The individual Query Filter model - * @return {number} A numeric value by which the model should be ordered relative to others. + * @returns {number} A numeric value by which the model should be ordered relative to others. */ - comparator: function (model) { + comparator(model) { try { - var categoriesMap = model.categoriesMap(); - var order = _(categoriesMap) + const categoriesMap = model.categoriesMap(); + const order = _(categoriesMap) .chain() .pluck("queryFields") .flatten() .value(); return order.indexOf(model.get("name")); - } catch (e) { - console.log( - "Failed to sort the Query Fields Collection, error message: " + e, - ); + } catch { return 0; } }, /** * The constructed URL of the collection - * * @returns {string} - The URL to use during fetch */ - url: function () { + url() { try { return MetacatUI.appModel.get("queryServiceUrl").replace(/\/\?$/, ""); } catch (e) { @@ -83,39 +61,31 @@ define([ /** * Retrieve the fields from the Coordinating Node - * @extends Backbone.Collection#fetch + * @param {object} options Options to pass to the fetch method + * @augments Backbone.Collection#fetch + * @returns {Array} The array of Query Field attributes to be added to the collection. */ - fetch: function (options) { - try { - var fetchOptions = _.extend({ dataType: "text" }, options); - return Backbone.Model.prototype.fetch.call(this, fetchOptions); - } catch (e) { - console.log("Failed to fetch Query Fields, error message: " + e); - } + fetch(options) { + const fetchOptions = _.extend({ dataType: "text" }, options); + return Backbone.Model.prototype.fetch.call(this, fetchOptions); }, /** * parse - Parse the XML response from the CN - * * @param {string} response The queryEngineDescription XML as a string - * @return {Array} the Array of Query Field attributes to be added to the collection. + * @returns {Array} the Array of Query Field attributes to be added to the collection. */ - parse: function (response) { - try { - // If the collection is already parsed, just return it - if (typeof response === "object") { - return response; - } - var x2js = new X2JS(); - var responseJSON = x2js.xml_str2json(response); - if (responseJSON && responseJSON.queryEngineDescription) { - return responseJSON.queryEngineDescription.queryField; - } - } catch (e) { - console.log( - "Failed to parse Query Fields response, error message: " + e, - ); + parse(response) { + // If the collection is already parsed, just return it + if (typeof response === "object") { + return response; + } + const x2js = new X2JS(); + const responseJSON = x2js.xml_str2json(response); + if (responseJSON && responseJSON.queryEngineDescription) { + return responseJSON.queryEngineDescription.queryField; } + return []; }, /** @@ -124,44 +94,41 @@ define([ * type text, use a regular filter model. If the fields are tdate, use a * dateFilter. If the field types are mixed, then returns the filterType default * value in QueryField models. - * * @param {string[]} fields The list of Query Field names - * @return {string} The nodeName of the filter model to use (one of the four types + * @returns {string} The nodeName of the filter model to use (one of the four types * of fields that are set in {@link QueryField#filterTypesMap}) */ - getRequiredFilterType: function (fields) { - try { - var types = [], - // When fields is empty or are different types - defaultFilterType = - MetacatUI.queryFields.models[0].defaults().filterType; + getRequiredFilterType(fields) { + const defaultFilterType = + MetacatUI.queryFields.models[0].defaults().filterType; - if (!fields || fields.length === 0 || fields[0] === "") { - return defaultFilterType; - } + const types = []; + // When fields is empty or are different types - fields.forEach((newField, i) => { - var fieldModel = MetacatUI.queryFields.findWhere({ - name: newField, - }); - types.push(fieldModel.get("filterType")); - }); - - // Test of all the fields are of the same type - var allEqual = types.every((val, i, arr) => val === arr[0]); + if (!fields || fields.length === 0 || fields[0] === "") { + return defaultFilterType; + } - if (allEqual) { - return types[0]; + fields.forEach((newField) => { + const fieldModel = MetacatUI.queryFields.findWhere({ + name: newField, + }); + const newType = fieldModel?.get("filterType"); + if (newType) { + types.push(newType); } else { - return defaultFilterType; + // TODO: + // console.log("ERROR! No filter type found for field", newField); } - } catch (e) { - console.log( - "Failed to detect the required filter type in a Query Fields" + - " Collection, error message: " + - e, - ); + }); + + // Test of all the fields are of the same type + const allEqual = types.every((val, i, arr) => val === arr[0]); + + if (allEqual) { + return types[0]; } + return defaultFilterType; }, }, ); From e90a552584500b0a6005bd700f50c7559d0e2af9 Mon Sep 17 00:00:00 2001 From: robyngit Date: Tue, 2 Jul 2024 16:09:48 -0400 Subject: [PATCH 058/169] Fix linting errors in QueryRuleView Issue #2253 --- src/js/views/queryBuilder/QueryRuleView.js | 1735 ++++++++------------ 1 file changed, 729 insertions(+), 1006 deletions(-) diff --git a/src/js/views/queryBuilder/QueryRuleView.js b/src/js/views/queryBuilder/QueryRuleView.js index f56243d4c..249c19af7 100644 --- a/src/js/views/queryBuilder/QueryRuleView.js +++ b/src/js/views/queryBuilder/QueryRuleView.js @@ -10,11 +10,7 @@ define([ "views/filters/DateFilterView", "views/searchSelect/ObjectFormatSelectView", "views/searchSelect/AnnotationFilterView", - "models/filters/Filter", - "models/filters/BooleanFilter", - "models/filters/NumericFilter", - "models/filters/DateFilter", -], function ( +], ( $, _, Backbone, @@ -26,22 +22,18 @@ define([ DateFilterView, ObjectFormatSelect, AnnotationFilter, - Filter, - BooleanFilter, - NumericFilter, - DateFilter, -) { +) => /** * @class QueryRuleView * @classdesc A view that provides an UI for a user to construct a single filter that * is part of a complex query * @classcategory Views/QueryBuilder * @screenshot views/QueryRuleView.png - * @extends Backbone.View - * @constructor + * @augments Backbone.View + * @class * @since 2.14.0 */ - return Backbone.View.extend( + Backbone.View.extend( /** @lends QueryRuleView.prototype */ { /** @@ -146,14 +138,14 @@ define([ /** * A function that creates and returns the Backbone events object. - * @return {Object} Returns a Backbone events object + * @returns {object} Returns a Backbone events object */ - events: function () { - var events = {}; - var removeID = "#" + this.removeRuleID + this.cid; - events["click " + removeID] = "removeSelf"; - events["mouseover " + removeID] = "previewRemove"; - events["mouseout " + removeID] = "previewRemove"; + events() { + const events = {}; + const removeID = `#${this.removeRuleID}${this.cid}`; + events[`click ${removeID}`] = "removeSelf"; + events[`mouseover ${removeID}`] = "previewRemove"; + events[`mouseout ${removeID}`] = "previewRemove"; return events; }, @@ -163,9 +155,7 @@ define([ * abstracted fields which are a combination of multiple query fields, or to add a * duplicate field that has a different label. These special fields are passed on * to {@link QueryFieldSelectView#addFields}. - * * @type {SpecialField[]} - * * @since 2.15.0 */ specialFields: [ @@ -198,8 +188,7 @@ define([ * when a user selects a new operator. Operators can set the exclude and * matchSubstring properties of the model, and sometimes the values as well. * Either the types property OR the fields property must be set, not both. - * - * @typedef {Object} OperatorOption + * @typedef {object} OperatorOption * @property {string} label - The label to display to the user * @property {string} icon - An icon that represents the operator * @property {boolean} matchSubstring - Whether the matchSubstring attribute is @@ -225,7 +214,6 @@ define([ /** * The list of operators that will be available in the dropdown list that connects * the query fields to the values. Each operator must be unique. - * * @type {OperatorOption[]} */ operatorOptions: [ @@ -390,8 +378,7 @@ define([ * different solr query fields, and so we display different interfaces depending * on the type and category of the selected query fields. A Value Input Option * object defines a of interface to show for a given type and category. - * - * @typedef {Object} ValueInputOption + * @typedef {object} ValueInputOption * @property {string[]} filterTypes - An array of one or more filter types that * are allowed for this interface. If none are provided then any filter type is * allowed. Filter types are one of the four keys defined in @@ -407,7 +394,7 @@ define([ * the special field, not the actual query fields that it represents. * @property {string} label - If the interface does not include a label (e.g. * number filter), include a string to display here. - * @property {function} uiFunction - A function that returns the UI view to use + * @property {Function} uiFunction - A function that returns the UI view to use * with all appropriate options set. The function will be called with this view as * the context. */ @@ -424,7 +411,7 @@ define([ // serviceCoupling field { queryFields: ["serviceCoupling"], - uiFunction: function () { + uiFunction() { return new SearchableSelect({ options: [ { @@ -456,7 +443,7 @@ define([ // Metadata format IDs { queryFields: ["formatId"], - uiFunction: function () { + uiFunction() { return new ObjectFormatSelect({ selected: this.model.get("values"), separatorText: this.model.get("operator"), @@ -466,7 +453,7 @@ define([ // Semantic annotation picker { queryFields: ["sem_annotation"], - uiFunction: function () { + uiFunction() { // A bioportalAPIKey is required for the Annotation Filter UI if (MetacatUI.appModel.get("bioportalAPIKey")) { return new AnnotationFilter({ @@ -476,9 +463,8 @@ define([ inputLabel: "Type a value", }); // If there's no API key, render the default UI (the last in this list) - } else { - return this.valueSelectUImap.slice(-1)[0].uiFunction.call(this); } + return this.valueSelectUImap.slice(-1)[0].uiFunction.call(this); }, }, // User/Organization account ID lookup @@ -490,7 +476,7 @@ define([ "rightsHolder", "submitter", ], - uiFunction: function () { + uiFunction() { return new AccountSelect({ selected: this.model.get("values"), separatorText: this.model.get("operator"), @@ -507,7 +493,7 @@ define([ "authoritativeMN", "datasource", ], - uiFunction: function () { + uiFunction() { return new NodeSelect({ selected: this.model.get("values"), separatorText: this.model.get("operator"), @@ -518,7 +504,7 @@ define([ { filterTypes: ["numericFilter"], label: "Choose a value", - uiFunction: function () { + uiFunction() { return new NumericFilterView({ model: this.model, showButton: false, @@ -530,7 +516,7 @@ define([ { filterTypes: ["dateFilter"], label: "Choose a year", - uiFunction: function () { + uiFunction() { return new DateFilterView({ model: this.model, separatorText: this.model.get("operator"), @@ -539,7 +525,7 @@ define([ }, // The last is the default value selection UI { - uiFunction: function () { + uiFunction() { return new SearchableSelect({ options: [], allowMulti: true, @@ -554,191 +540,153 @@ define([ /** * Creates a new QueryRuleView - * @param {Object} options - A literal object with options to pass to the view + * @param {object} options - A literal object with options to pass to the view */ - initialize: function (options) { - try { - // Get all the options and apply them to this view - if (typeof options == "object") { - var optionKeys = Object.keys(options); - _.each( - optionKeys, - function (key, i) { - this[key] = options[key]; - }, - this, - ); - } - - // If no model is provided in the options, we cannot render this view. A - // filter model cannot be created, because it must be part of a collection. - if (!this.model || !this.model.collection) { - console.error( - "error: A Filter model that's part of a Filters collection" + - " is required to initialize a Query Rule view.", - ); - return; - } + initialize(options) { + // Apply all the options to this view + if (typeof options === "object") { + Object.assign(this, options); + } - // The model may be removed during the save process if it's empty. Remove this - // Rule Group view when that happens. - this.stopListening(this.model, "remove"); - this.listenTo(this.model, "remove", function () { - this.removeSelf(); - }); - } catch (e) { - console.log( - "Failed to initialize a Query Builder View, error message:", - e, + // If no model is provided in the options, we cannot render this view. A + // filter model cannot be created, because it must be part of a collection. + if (!this.model || !this.model.collection) { + throw new Error( + "A Filter model that's part of a Filters collection is required to initialize a Query Rule view.", ); } + + // The model may be removed during the save process if it's empty. Remove this + // Rule Group view when that happens. + this.stopListening(this.model, "remove"); + this.listenTo(this.model, "remove", () => { + this.removeSelf(); + }); }, /** * render - Render the view - * - * @return {QueryRule} Returns the view + * @returns {QueryRule} Returns the view */ - render: function () { - try { - // Add the Rule number. - // TODO: Also add the number of datasets related to rule - this.addRuleInfo(); - this.stopListening(this.model.collection, "remove"); - this.listenTo(this.model.collection, "remove", this.updateRuleInfo); - // Nested rules should also listen for changes in Filters of their parent Rule - if (this.parentRule) { - this.stopListening(this.parentRule.model.collection, "remove"); - this.listenTo( - this.parentRule.model.collection, - "remove", - this.updateRuleInfo, - ); - } - - // The remove button is needed for both FilterGroups and other Filter models - this.addRemoveButton(); - - // Render nested filter group views as another Query Builder. - if (this.model.type == "FilterGroup") { - this.$el.addClass("rule-group"); - - // We must initialize a QueryBuilderView using the inline require syntax to - // avoid the problem of circular dependencies. QueryRuleView requires - // QueryBuilderView, and QueryBuilderView requires QueryRuleView. For more - // info, see https://requirejs.org/docs/api.html#circular - var QueryBuilderView = require("views/queryBuilder/QueryBuilderView"); - - // The default - nestedLevelsAllowed = 1; - // If we are adding a query builer, then it is a nested level. Subtract one - // from the total levels allowed. - if (typeof this.nestedLevelsAllowed == "number") { - nestedLevelsAllowed = this.nestedLevelsAllowed - 1; - } + render() { + // Add the Rule number. + // TODO: Also add the number of datasets related to rule + this.addRuleInfo(); + this.stopListening(this.model.collection, "remove"); + this.listenTo(this.model.collection, "remove", this.updateRuleInfo); + // Nested rules should also listen for changes in Filters of their parent Rule + if (this.parentRule) { + this.stopListening(this.parentRule.model.collection, "remove"); + this.listenTo( + this.parentRule.model.collection, + "remove", + this.updateRuleInfo, + ); + } - // If there is a special list of fields to exclude in nested Query Builders - // (i.e. in nested FilterGroup models), then pass this list on as the - // excludeFields list in the child QueryBuilder - var excludeFields = this.excludeFields; - if ( - this.nestedExcludeFields && - Array.isArray(this.nestedExcludeFields) - ) { - excludeFields = this.nestedExcludeFields; - } + // The remove button is needed for both FilterGroups and other Filter models + this.addRemoveButton(); + + // Render nested filter group views as another Query Builder. + if (this.model.type === "FilterGroup") { + this.$el.addClass("rule-group"); + + // We must initialize a QueryBuilderView using the inline require syntax to + // avoid the problem of circular dependencies. QueryRuleView requires + // QueryBuilderView, and QueryBuilderView requires QueryRuleView. For more + // info, see https://requirejs.org/docs/api.html#circular + const QueryBuilderView = require("views/queryBuilder/QueryBuilderView"); + + // The default + let nestedLevelsAllowed = 1; + // If we are adding a query builer, then it is a nested level. Subtract one + // from the total levels allowed. + if (typeof this.nestedLevelsAllowed === "number") { + nestedLevelsAllowed = this.nestedLevelsAllowed - 1; + } - // Insert QueryRuleView - var ruleGroup = new QueryBuilderView({ - filterGroup: this.model, - // Nested Query Rules have the same color as their parent rule - ruleColorPalette: "inherit", - excludeFields: excludeFields, - specialFields: this.specialFields, - parentRule: this, - nestedLevelsAllowed: nestedLevelsAllowed, - }); - this.el.append(ruleGroup.el); - ruleGroup.render(); - } else { - // For any other filter type... Add a metadata selector field whether the - // rule is new or has already been created - this.addFieldSelect(); - - // Operator field and value field Add an operator input only for already - // existing filters (For new filters, a metadata field needs to be selected - // first) - if (this.model.get("fields") && this.model.get("fields").length) { - this.addOperatorSelect(); - this.addValueSelect(); - } + // If there is a special list of fields to exclude in nested Query Builders + // (i.e. in nested FilterGroup models), then pass this list on as the + // excludeFields list in the child QueryBuilder + let { excludeFields } = this; + if ( + this.nestedExcludeFields && + Array.isArray(this.nestedExcludeFields) + ) { + excludeFields = this.nestedExcludeFields; } - return this; - } catch (e) { - console.error( - "Error rendering the query Rule View, error message: ", - e, - ); + // Insert QueryRuleView + const ruleGroup = new QueryBuilderView({ + filterGroup: this.model, + // Nested Query Rules have the same color as their parent rule + ruleColorPalette: "inherit", + excludeFields, + specialFields: this.specialFields, + parentRule: this, + nestedLevelsAllowed, + }); + this.el.append(ruleGroup.el); + ruleGroup.render(); + } else { + // For any other filter type... Add a metadata selector field whether the + // rule is new or has already been created + this.addFieldSelect(); + + // Operator field and value field Add an operator input only for already + // existing filters (For new filters, a metadata field needs to be selected + // first) + if (this.model.get("fields") && this.model.get("fields").length) { + this.addOperatorSelect(); + this.addValueSelect(); + } } + + return this; }, /** * Insert container for the color-coded rule numbering. */ - addRuleInfo: function () { - try { - this.$indexEl = $(document.createElement("span")); - this.$ruleInfoEl = $(document.createElement("div")).addClass( - this.ruleInfoClass, - ); - this.$ruleInfoEl.append(this.$indexEl); + addRuleInfo() { + this.$indexEl = $(document.createElement("span")); + this.$ruleInfoEl = $(document.createElement("div")).addClass( + this.ruleInfoClass, + ); + this.$ruleInfoEl.append(this.$indexEl); - this.$el.append(this.$ruleInfoEl); - this.updateRuleInfo(); - } catch (error) { - console.log( - "Error adding rule info container for a Query Rule, details: " + - error, - ); - } + this.$el.append(this.$ruleInfoEl); + this.updateRuleInfo(); }, /** - * Selects a color from the - * {@link QueryRuleView#ruleColorPalette rule colour palette array}, given an + * Selects a color from the {@link QueryRuleView#ruleColorPalette}, given an * index. If the index is greater than the length of the palette, then the palette * is effectively repeated until long enough (i.e. colours will be recycled). If * no index in provided, the first colour in the palette will be selected. - * - * @param {number} [index=0] - The position of the rule within the Filters + * @param {number} [index] - The position of the rule within the Filters * collection. - * @param {string} [defaultColor="#57b39c"] - A default colour to use in case + * @param {string} [defaultColor] - A default colour to use in case * there is problem with this function (hex color code beginning with '#'). - * @return {string} - Returns a hex color code string + * @returns {string} - Returns a hex color code string */ - getPaletteColor: function (index = 0, defaultColor = "#57b39c") { + getPaletteColor(index = 0, defaultColor = "#57b39c") { try { - // Allow the rule to inherit it's color from the parent rule within which it's + // Allow the rule to inherit its color from the parent rule within which it's // nested - if (this.ruleColorPalette == "inherit") { + if (this.ruleColorPalette === "inherit") { return null; } + if (!this.ruleColorPalette || !this.ruleColorPalette.length) { return defaultColor; } - var numCols = this.ruleColorPalette.length; - if (index + 1 > numCols) { - var n = Math.floor(index / numCols); - index = index - numCols * n; - } - return this.ruleColorPalette[index]; - } catch (error) { - console.log( - "Error getting a color for a Query Rule, using the default colour" + - " instead. Error details: " + - error, - ); + + const numCols = this.ruleColorPalette.length; + const adjustedIndex = index % numCols; + + return this.ruleColorPalette[adjustedIndex]; + } catch { return defaultColor; } }, @@ -749,74 +697,55 @@ define([ * the rule number, but may one day also display information such as the number of * results that there are for this individual rule. */ - updateRuleInfo: function () { - try { - // Rules are numbered in the order in which they appear in the Filters - // collection, excluding any invisible filter models. Rules nested in Rule - // Groups (within Filter Models) get numbered 3A, 3B, etc. - var letter = ""; - var index = ""; - // If this is a filter model nested in a filter group - if (this.parentRule) { - index = this.parentRule.ruleNumber; - var letterIndex = this.model.collection.visibleIndexOf(this.model); - if (typeof letterIndex === "number") { - letter = String.fromCharCode(94 + letterIndex + 3).toUpperCase(); - } - // For top-level filter models - } else { - index = this.model.collection.visibleIndexOf(this.model); + updateRuleInfo() { + // Rules are numbered in the order in which they appear in the Filters + // collection, excluding any invisible filter models. Rules nested in Rule + // Groups (within Filter Models) get numbered 3A, 3B, etc. + let letter = ""; + let index = ""; + // If this is a filter model nested in a filter group + if (this.parentRule) { + index = this.parentRule.ruleNumber; + const letterIndex = this.model.collection.visibleIndexOf(this.model); + if (typeof letterIndex === "number") { + letter = String.fromCharCode(94 + letterIndex + 3).toUpperCase(); } + // For top-level filter models + } else { + index = this.model.collection.visibleIndexOf(this.model); + } - if (typeof index == "number") { - index = index + 1; - } + if (typeof index === "number") { + index += 1; + } - var ruleNumber = index + letter; + const ruleNumber = index + letter; - // Set the rule number of the parent view to be accessed by any nested child - // rules - this.ruleNumber = ruleNumber; + // Set the rule number of the parent view to be accessed by any nested child + // rules + this.ruleNumber = ruleNumber; - // if(this.model.type == "FilterGroup") - if (ruleNumber && ruleNumber.length) { - this.$indexEl.text("Rule " + ruleNumber); - } else { - this.$indexEl.text(""); - return; - } - var color = this.getPaletteColor(index); - if (color) { - this.el.style.setProperty("--rule-color", color); - } - } catch (error) { - console.log( - "Error updating the rule numbering for a Query Rule. Details: " + - error, - ); + // if(this.model.type == "FilterGroup") + if (ruleNumber && ruleNumber.length) { + this.$indexEl.text(`Rule ${ruleNumber}`); + } else { + this.$indexEl.text(""); + return; + } + const color = this.getPaletteColor(index); + if (color) { + this.el.style.setProperty("--rule-color", color); } }, /** * addRemoveButton - Create and insert the button to remove the Query Rule */ - addRemoveButton: function () { - try { - var removeButton = $( - "", - ); - this.el.append(removeButton[0]); - } catch (e) { - console.error( - "Failed to create a remove button for a Query Rule, error details: " + - e, - ); - } + addRemoveButton() { + const removeButton = $( + ``, + ); + this.el.append(removeButton[0]); }, /** @@ -827,36 +756,34 @@ define([ * in the special field's "fields" property. If the special field has an array set * for "values", then the model's values must also exactly match the special * field's values. - * * @param {string[]} [fields] - Optionally set a list of query fields to search * with. If not set, then the fields that are set on the view's filter model are * used. * @returns {SpecialField|null} - The matching special field, or null if no match * was found. - * * @since 2.15.0 */ - getSpecialField: function (fields) { + getSpecialField(fields) { // Get information about the filter model (or used the fields passed to this // function) - var selectedFields = fields || this.model.get("fields"); - var selectedFields = _.clone(selectedFields); - var selectedValues = this.model.get("values"); + const originalSelectedFields = fields || this.model.get("fields"); + const selectedFields = _.clone(originalSelectedFields); + const selectedValues = this.model.get("values"); if (!this.specialFields || !Array.isArray(this.specialFields)) { return null; } - var matchingSpecialField = _.find( + const matchingSpecialField = _.find( this.specialFields, - function (specialField) { - var fieldsMatch = false, - mustMatchValues = false, - valuesMatch = false; + (specialField) => { + let fieldsMatch = false; + let mustMatchValues = false; + let valuesMatch = false; // If *all* the fields in the fields array are present in the list // of fields that the special field represents, then count this as a match. - var commonFields = _.intersection( + const commonFields = _.intersection( specialField.fields, selectedFields, ); @@ -882,73 +809,50 @@ define([ // If this model matches one of the special fields, render it differently return matchingSpecialField || null; }, - /** - * Takes a list of query field names, checks if the model matches any of the - * special fields, and if it does, returns the list of fields with the actual - * field names replaced with the - * {@link QueryRuleView#specialFields special field name}. This function is the - * opposite of {@link QueryRuleView#convertFromSpecialFields} - * @param {string[]} fields - The list of field names to convert - * @returns {string[]} - The converted list of field names. If there were no - * special fields detected, or if there's an error, then then the field names are - * returned unchanged. - * - * @param {string[]} fields - The list of fields to convert to special fields, if - * the model matches any of the special field objects - * @returns {string[]} - Returns the list of fields with actual query field names - * replaced with special field names, if any match - * - * @since 2.15.0 + * Converts a list of query field names to special field names based on matches + * from the special fields defined. If a field matches a special field's subfields, + * it is replaced by the special field name. + * @param {string[]} fields - The list of field names to convert + * @returns {string[]} - The converted list of field names. If no special fields are + * detected, then the field names are returned unchanged. */ - convertToSpecialFields: function (fields) { - try { - var fields = _.clone(fields); - - // Insert the special field name at the same position as the associated - // query fields that we will remove - var replaceWithSpecialField = function (fields, specialField) { - if (specialField) { - position = _.findIndex( - fields, - function (selectedField) { - return specialField.fields.includes(selectedField); - }, - this, - ); - fields.splice(position, 0, specialField.name); - fields = _.difference(fields, specialField.fields); - } - return fields; - }; + convertToSpecialFields(fields) { + let fieldsCopy = [...fields]; - // If the user selected a special field, make sure we convert those first - if (this.selectedSpecialFields && this.selectedSpecialFields.length) { - this.selectedSpecialFields.forEach(function (specialFiend) { - fields = replaceWithSpecialField(fields, specialFiend); - }, this); + // Helper function to replace fields with a special field name + const replaceWithSpecialField = (originalFields, specialField) => { + const position = originalFields.findIndex((field) => + specialField.fields.includes(field), + ); + if (position !== -1) { + originalFields.splice( + position, + specialField.fields.length, + specialField.name, + ); } + return originalFields; + }; - // Search for remaining special fields given the fields and model values - var matchingSpecialField = this.getSpecialField(fields); - - // There may be more than one special field in the list of fields... - while (matchingSpecialField !== null) { - fields = replaceWithSpecialField(fields, matchingSpecialField); - // Check if there are more special fields remaining - matchingSpecialField = this.getSpecialField(fields); - } + // Iterate over each selected special field to transform the fields array + if (this.selectedSpecialFields && this.selectedSpecialFields.length) { + this.selectedSpecialFields.forEach((specialField) => { + fieldsCopy = replaceWithSpecialField(fieldsCopy, specialField); + }); + } - return fields; - } catch (error) { - console.log( - "Error converting query field names to special field names in" + - " a Query Rule View. Returning the list of fields unchanged." + - " Error details : " + - error, + // Replace any remaining matches + let matchingSpecialField = this.getSpecialField(fieldsCopy); + while (matchingSpecialField) { + fieldsCopy = replaceWithSpecialField( + fieldsCopy, + matchingSpecialField, ); - return fields; + matchingSpecialField = this.getSpecialField(fieldsCopy); } + + return fieldsCopy; }, /** @@ -959,43 +863,30 @@ define([ * array set on the view's selectedSpecialFields property. selectedSpecialFields * is cleared each time this function runs. This function is the opposite of * {@link QueryRuleView#convertToSpecialFields} - * @param {string[]} fields] - The list of field names to convert - * @returns {string[]} - The converted list of field names. If there were no - * special fields detected, or if there's an error, then then the field names are - * returned unchanged. - * * @param {string[]} fields - The list of fields to convert to actual query * service index fields * @returns {string[]} - Returns the list of fields with any special field - * replaced with real fields from the query service index - * + * replaced with real fields from the query service index. If there were no + * special fields detected, or if there's an error, then then the field names are + * returned unchanged. * @since 2.15.0 */ - convertFromSpecialFields: function (fields) { + convertFromSpecialFields(fields) { try { this.selectedSpecialFields = []; if (this.specialFields) { - this.specialFields.forEach(function (specialField) { - var index = fields.indexOf(specialField.name); + this.specialFields.forEach((specialField) => { + const index = fields.indexOf(specialField.name); if (index >= 0) { // Keep a record that the user selected a special field (useful in the // case that the special field is just a duplicate of another field) this.selectedSpecialFields.push(specialField); - fields.splice.apply( - fields, - [index, 1].concat(specialField.fields), - ); + fields.splice(index, 1, ...specialField.fields); } }, this); } return fields; } catch (error) { - console.log( - "Error converting special query fields to query fields that" + - " exist in the index in a Query Rule View. Returning the fields" + - " unchanged. Error details: " + - error, - ); return fields; } }, @@ -1004,793 +895,630 @@ define([ * Create and insert an input that allows the user to select a metadata field to * query */ - addFieldSelect: function () { - try { - // Check whether the filter model set on this view contains query fields - // and values that match one of the special rules. If it does, - // convert the list of field names to special field to pass on to the - // Query Field Select View. - var selectedFields = _.clone(this.model.get("fields")); - var selectedFields = this.convertToSpecialFields(selectedFields); - - this.fieldSelect = new QueryFieldSelect({ - selected: selectedFields, - excludeFields: this.excludeFields, - addFields: this.specialFields, - separatorText: this.model.get("fieldsOperator"), - }); - this.fieldSelect.$el.addClass(this.fieldsClass); - this.el.append(this.fieldSelect.el); - this.fieldSelect.render(); - - // Update the model when the fieldsOperator changes - this.stopListening(this.fieldSelect, "separatorChanged"); - this.listenTo( - this.fieldSelect, - "separatorChanged", - function (newOperator) { - this.model.set("fieldsOperator", newOperator); - }, - ); - // Update model when the selected fields change - this.stopListening(this.fieldSelect, "changeSelection"); - this.listenTo( - this.fieldSelect, - "changeSelection", - this.handleFieldChange, - ); - } catch (e) { - console.error( - "Error adding a metadata selector input in the Query Rule" + - " View, error message:", - e, - ); - } + addFieldSelect() { + // Check whether the filter model set on this view contains query fields + // and values that match one of the special rules. If it does, + // convert the list of field names to special field to pass on to the + // Query Field Select View. + let selectedFields = _.clone(this.model.get("fields")); + selectedFields = this.convertToSpecialFields(selectedFields); + + this.fieldSelect = new QueryFieldSelect({ + selected: selectedFields, + excludeFields: this.excludeFields, + addFields: this.specialFields, + separatorText: this.model.get("fieldsOperator"), + }); + this.fieldSelect.$el.addClass(this.fieldsClass); + this.el.append(this.fieldSelect.el); + this.fieldSelect.render(); + + // Update the model when the fieldsOperator changes + this.stopListening(this.fieldSelect, "separatorChanged"); + this.listenTo(this.fieldSelect, "separatorChanged", (newOperator) => { + this.model.set("fieldsOperator", newOperator); + }); + // Update model when the selected fields change + this.stopListening(this.fieldSelect, "changeSelection"); + this.listenTo( + this.fieldSelect, + "changeSelection", + this.handleFieldChange, + ); }, /** * handleFieldChange - Called when the Query Field Select View triggers a change * event. Updates the model with the new fields, and if required, * 1) converts the filter model to a different type based on the types of fields - * selected, 2) updates the operator select and the value select - * - * @param {string[]} newFields The list of new query fields that were selected + * selected, 2) updates the operator select and the value select + * @param {string[]} fields The list of new query fields that were selected */ - handleFieldChange: function (newFields) { - try { - // Uncomment the following chunk to clear operator & values when the field - // input is cleared. - // if(!newFields || newFields.length === 0 || newFields[0] === ""){ - // if(this.operatorSelect){ - // this.operatorSelect.changeSelection([""]); - // } - // this.model.set("fields", this.model.defaults().fields); - // return - // } - - // Get the selected operator before the field changed - var opBefore = this.getSelectedOperator(); - - // If any of the new fields are special fields, replace them with the - // actual query fields before setting them in the model... - newFields = this.convertFromSpecialFields(newFields); - - // Get the current type of filter and required type given the newly selected - // fields - var typeBefore = this.model.get("nodeName"), - typeAfter = MetacatUI.queryFields.getRequiredFilterType(newFields); - - // If the type has changed, then replace the model with one of the correct - // type, update the value and operator inputs, and do nothing else - if (typeBefore != typeAfter) { - this.model = this.model.collection.replaceModel(this.model, { - filterType: typeAfter, - fields: newFields, - }); - this.removeInput("value"); - this.removeInput("operator"); - this.addOperatorSelect(""); - return; - } + handleFieldChange(fields) { + // Get the selected operator before the field changed + const opBefore = this.getSelectedOperator(); + + // If any of the new fields are special fields, replace them with the + // actual query fields before setting them in the model... + const newFields = this.convertFromSpecialFields(fields); + + // Get the current type of filter and required type given the newly selected + // fields + const typeBefore = this.model.get("nodeName"); + const typeAfter = + MetacatUI.queryFields.getRequiredFilterType(newFields); + + // If the type has changed, then replace the model with one of the correct + // type, update the value and operator inputs, and do nothing else + if (typeBefore !== typeAfter) { + this.model = this.model.collection.replaceModel(this.model, { + filterType: typeAfter, + fields: newFields, + }); + this.removeInput("value"); + this.removeInput("operator"); + this.addOperatorSelect(""); + return; + } - // If the filter model type is the same, and the operator options are the same - // for the selected fields, then update the model - this.model.set("fields", newFields); - - // Get the selected operator now that we've updated the model with new fields - var opAfter = this.getSelectedOperator(); - - // Add an empty operator input field, if there isn't one - if (!this.operatorSelect) { - this.addOperatorSelect(""); - // If the operator options have changed, refresh the operator input - } else if (opAfter !== opBefore) { - this.removeInput("operator"); - // Make sure that we overwrite any values that don't apply to the new options. - this.handleOperatorChange([""]); - this.addOperatorSelect(""); - return; - } + // If the filter model type is the same, and the operator options are the same + // for the selected fields, then update the model + this.model.set("fields", newFields); + + // Get the selected operator now that we've updated the model with new fields + const opAfter = this.getSelectedOperator(); + + // Add an empty operator input field, if there isn't one + if (!this.operatorSelect) { + this.addOperatorSelect(""); + // If the operator options have changed, refresh the operator input + } else if (opAfter !== opBefore) { + this.removeInput("operator"); + // Make sure that we overwrite any values that don't apply to the new options. + this.handleOperatorChange([""]); + this.addOperatorSelect(""); + return; + } - // Refresh the value select in case a different value input is required for - // the new fields - if (this.valueSelect) { - this.removeInput("value"); - this.addValueSelect(); - } - } catch (e) { - console.error( - "Failed to handle query field change in the Query Rule View," + - " error message: " + - e, - ); + // Refresh the value select in case a different value input is required for + // the new fields + if (this.valueSelect) { + this.removeInput("value"); + this.addValueSelect(); } }, /** * Create and insert an input field where the user can select an operator for the * given rule. Operators will vary depending on filter model type. - * - * @param {string} selectedOperator - optional. The label of an operator to + * @param {string} operator - optional. The label of an operator to * pre-select. Set to an empty string to render an empty operator selector. */ - addOperatorSelect: function (selectedOperator) { - try { - var view = this; - var operatorError = false; - - var options = this.getOperatorOptions(); - - // Identify the selected operator for existing models - if (typeof selectedOperator !== "string") { - selectedOperator = this.getSelectedOperator(); - // If there was no operator found, then this is probably an unsupported - // combination of exclude + matchSubstring + filterType - if (selectedOperator === "") { - operatorError = true; - } - } - + addOperatorSelect(operator) { + const view = this; + const options = this.getOperatorOptions(); + let operatorError = false; + let selectedOperator = operator; + + // Identify the selected operator for existing models + if (typeof selectedOperator !== "string") { + selectedOperator = this.getSelectedOperator(); + // If there was no operator found, then this is probably an unsupported + // combination of exclude + matchSubstring + filterType if (selectedOperator === "") { - selectedOperator = []; - } else { - selectedOperator = [selectedOperator]; + operatorError = true; } + } + + if (selectedOperator === "") { + selectedOperator = []; + } else { + selectedOperator = [selectedOperator]; + } - this.operatorSelect = new SearchableSelect({ - options: options, - allowMulti: false, - inputLabel: "Select an operator", - clearable: false, - placeholderText: "Select an operator", - selected: selectedOperator, + this.operatorSelect = new SearchableSelect({ + options, + allowMulti: false, + inputLabel: "Select an operator", + clearable: false, + placeholderText: "Select an operator", + selected: selectedOperator, + }); + this.operatorSelect.$el.addClass(this.operatorClass); + this.el.append(this.operatorSelect.el); + + if (operatorError) { + view.listenToOnce(view.operatorSelect, "postRender", () => { + view.operatorSelect.showMessage( + "Please select a valid operator", + "error", + true, + ); }); - this.operatorSelect.$el.addClass(this.operatorClass); - this.el.append(this.operatorSelect.el); - - if (operatorError) { - view.listenToOnce(view.operatorSelect, "postRender", function () { - view.operatorSelect.showMessage( - "Please select a valid operator", - "error", - true, - ); - }); - } + } - this.operatorSelect.render(); + this.operatorSelect.render(); - // Update model when the values change - this.stopListening(this.operatorSelect, "changeSelection"); - this.listenTo( - this.operatorSelect, - "changeSelection", - this.handleOperatorChange, - ); - } catch (e) { - console.error( - "Error adding an operator selector input in the Query Rule " + - "View, error message:", - e, - ); - } + // Update model when the values change + this.stopListening(this.operatorSelect, "changeSelection"); + this.listenTo( + this.operatorSelect, + "changeSelection", + this.handleOperatorChange, + ); }, /** * handleOperatorChange - When the operator selection is changed, update the model * and re-set the value UI when required - * * @param {string[]} newOperatorLabel The new operator label within an array, * e.g. ["is greater than"] */ - handleOperatorChange: function (newOperatorLabel) { - try { - var view = this; - - if (!newOperatorLabel || newOperatorLabel[0] == "") { - var modelDefaults = this.model.defaults(); - this.model.set({ - min: modelDefaults.min, - max: modelDefaults.max, - values: modelDefaults.values, - }); - this.removeInput("value"); - return; - } - - // Get the properties of the newly selected operator. The newOperatorLabel - // will be an array with one value. Select only from the available options, - // since there may be multiple options with the same label in - // this.operatorOptions. - var options = this.getOperatorOptions(); - var operator = _.findWhere(options, { label: newOperatorLabel[0] }); - - // Gather information about which values are currently set on the model, and - // which are required - var // Type - type = view.model.get("nodeName"), - isNumeric = ["dateFilter", "numericFilter"].includes(type), - isRange = operator.hasMin && operator.hasMax, - // Values - modelValues = this.model.get("values"), - modelHasValues = modelValues - ? modelValues && modelValues.length - : false, - modelFirstValue = modelHasValues ? modelValues[0] : null, - modelValueInt = parseInt(modelFirstValue) - ? parseInt(modelFirstValue) - : null, - needsValue = - isNumeric && - !modelValueInt && - !operator.hasMin && - !operator.hasMax, - // Min - modelMin = this.model.get("min"), - modelHasMin = modelMin === 0 || modelMin, - needsMin = operator.hasMin && !modelHasMin, - // Max - modelMax = this.model.get("max"), - modelHasMax = modelMax === 0 || modelMax, - needsMax = operator.hasMax && !modelHasMax; - - // Some operator options include a specific value to be set on the model. For - // example, "is not empty", should set the model value to the "*" wildcard. - // For operators with these specific value requirements, update the filter - // model value and remove the value select input. - if (operator.values && operator.values.length) { - this.removeInput("value"); - this.model.set("values", operator.values); - // If the operator does not have a default value, then ensure that there is - // a value select available. - } else { - if (!this.valueSelect) { - this.model.set("values", view.model.defaults().values); - this.addValueSelect(); - } - } - - // Update the model with true or false for matchSubstring and exclude - ["matchSubstring", "exclude"].forEach((prop, i) => { - if (typeof operator[prop] !== "undefined") { - view.model.set(prop, operator[prop]); - } else { - view.model.set(prop, view.model.defaults()[prop]); - } + handleOperatorChange(newOperatorLabel) { + const view = this; + + if (!newOperatorLabel || newOperatorLabel[0] === "") { + const modelDefaults = this.model.defaults(); + this.model.set({ + min: modelDefaults.min, + max: modelDefaults.max, + values: modelDefaults.values, }); + this.removeInput("value"); + return; + } - // Set min & max values as required by the operator - // TODO - test this strategy with dates... - - // Add a minimum value if one is needed - if (needsMin) { - // Search for the min in the values, then in the max - if (modelValueInt || modelValueInt === 0) { - this.model.set("min", modelValueInt); - } else if (modelHasMax) { - this.model.set("min", modelMax); - } else { - this.model.set("min", 0); - } - } + // Get the properties of the newly selected operator. The newOperatorLabel + // will be an array with one value. Select only from the available options, + // since there may be multiple options with the same label in + // this.operatorOptions. + const options = this.getOperatorOptions(); + const operator = _.findWhere(options, { label: newOperatorLabel[0] }); + + // Gather information about which values are currently set on the model, and + // which are required + const // Type + type = view.model.get("nodeName"); + const isNumeric = ["dateFilter", "numericFilter"].includes(type); + const isRange = operator.hasMin && operator.hasMax; + // Values + const modelValues = this.model.get("values"); + const modelHasValues = modelValues + ? modelValues && modelValues.length + : false; + const modelFirstValue = modelHasValues ? modelValues[0] : null; + const modelValueInt = parseInt(modelFirstValue, 10) + ? parseInt(modelFirstValue, 10) + : null; + const needsValue = + isNumeric && !modelValueInt && !operator.hasMin && !operator.hasMax; + // Min + const modelMin = this.model.get("min"); + const modelHasMin = modelMin === 0 || modelMin; + const needsMin = operator.hasMin && !modelHasMin; + // Max + const modelMax = this.model.get("max"); + const modelHasMax = modelMax === 0 || modelMax; + const needsMax = operator.hasMax && !modelHasMax; + + // Some operator options include a specific value to be set on the model. For + // example, "is not empty", should set the model value to the "*" wildcard. + // For operators with these specific value requirements, update the filter + // model value and remove the value select input. + if (operator.values && operator.values.length) { + this.removeInput("value"); + this.model.set("values", operator.values); + // If the operator does not have a default value, then ensure that there is + // a value select available. + } else if (!this.valueSelect) { + this.model.set("values", view.model.defaults().values); + this.addValueSelect(); + } - // Add a maximum value if one is needed - if (needsMax) { - // Search for the min in the values, then in the max - if (modelValueInt || modelValueInt === 0) { - this.model.set("max", modelValueInt); - } else if (modelHasMin) { - this.model.set("max", modelMin); - } else { - this.model.set("max", 0); - } + // Update the model with true or false for matchSubstring and exclude + ["matchSubstring", "exclude"].forEach((prop) => { + if (typeof operator[prop] !== "undefined") { + view.model.set(prop, operator[prop]); + } else { + view.model.set(prop, view.model.defaults()[prop]); } - - // Add a value if one is needed - if (needsValue) { - if (modelHasMin) { - this.model.set("values", [modelMin]); - } else if (modelHasMax) { - this.model.set("values", [modelMax]); - } else { - this.model.set("values", [0]); - } + }); + + // Set min & max values as required by the operator + // TODO - test this strategy with dates... + + // Add a minimum value if one is needed + if (needsMin) { + // Search for the min in the values, then in the max + if (modelValueInt || modelValueInt === 0) { + this.model.set("min", modelValueInt); + } else if (modelHasMax) { + this.model.set("min", modelMax); + } else { + this.model.set("min", 0); } + } - // Remove the minimum and max if they should not be included in the filter - if (modelHasMax && !operator.hasMax) { - this.model.set("max", this.model.defaults().max); - } - if (modelHasMin && !operator.hasMin) { - this.model.set("min", this.model.defaults().min); + // Add a maximum value if one is needed + if (needsMax) { + // Search for the min in the values, then in the max + if (modelValueInt || modelValueInt === 0) { + this.model.set("max", modelValueInt); + } else if (modelHasMin) { + this.model.set("max", modelMin); + } else { + this.model.set("max", 0); } + } - if (isRange) { - this.model.set("range", true); + // Add a value if one is needed + if (needsValue) { + if (modelHasMin) { + this.model.set("values", [modelMin]); + } else if (modelHasMax) { + this.model.set("values", [modelMax]); } else { - if (isNumeric) { - this.model.set("range", false); - } else { - this.model.unset("range"); - } + this.model.set("values", [0]); } + } - // If the operator changed for a numeric or date field, reset the value - // select. This way it can change from a range to a single value input if - // needed. - if (isNumeric) { - this.removeInput("value"); - this.addValueSelect(); - } - } catch (e) { - console.error( - "Failed to handle the operator selection in a Query Rule " + - "view, error message: " + - e, - ); + // Remove the minimum and max if they should not be included in the filter + if (modelHasMax && !operator.hasMax) { + this.model.set("max", this.model.defaults().max); + } + if (modelHasMin && !operator.hasMin) { + this.model.set("min", this.model.defaults().min); + } + + if (isRange) { + this.model.set("range", true); + } else if (isNumeric) { + this.model.set("range", false); + } else { + this.model.unset("range"); + } + + // If the operator changed for a numeric or date field, reset the value + // select. This way it can change from a range to a single value input if + // needed. + if (isNumeric) { + this.removeInput("value"); + this.addValueSelect(); } }, /** * Get a list of {@link QueryRuleView#operatorOptions operatorOptions} that are * allowed for this view's filter model - * - * @param {string[]} [fields] - Optional list of fields to use instead of the + * @param {string[]} [inputFields] - Optional list of fields to use instead of the * fields set on this view's Filter model - * + * @returns {object[]} - Returns an array of operator options that are allowed for + * this view's filter model * @since 2.15.0 */ - getOperatorOptions: function (fields) { - try { - // Check which type of rule this is (boolean, numeric, text, date) - var type = this.model.get("nodeName"); - - // If this rule contains a special field, replace the real query field names - // with the special field names for the purpose of selecting operator options - var fields = fields || this.model.get("fields"); - var fields = _.clone(fields); - var fields = this.convertToSpecialFields(fields); - - // Get the list of options for a user to select from based on field name. - // All of the rule's fields must be contained within the operator option's - // list of allowed fields for it to be a match. - var options = _.filter(this.operatorOptions, function (option) { - if (option.fields) { - return _.every(fields, function (fieldName) { - return option.fields.includes(fieldName); - }); - } - }); - - // Get the list of options for a user to select from based on type, if there - // were none that matched based on field names - if (!options || !options.length) { - options = _.filter( - this.operatorOptions, - function (option) { - if (option.types) { - return option.types.includes(type); - } - }, - this, + getOperatorOptions(inputFields) { + // Check which type of rule this is (boolean, numeric, text, date) + const type = this.model.get("nodeName"); + + // If this rule contains a special field, replace the real query field names + // with the special field names for the purpose of selecting operator options + let fields = inputFields || this.model.get("fields"); + fields = _.clone(fields); + fields = this.convertToSpecialFields(fields); + + // Get the list of options for a user to select from based on field name. + // All of the rule's fields must be contained within the operator option's + // list of allowed fields for it to be a match. + let options = _.filter(this.operatorOptions, (option) => { + if (option.fields) { + return _.every(fields, (fieldName) => + option.fields.includes(fieldName), ); } + return false; + }); - return options; - } catch (error) { - console.log( - "Error getting operator options in a Query Rule View, " + - "Error details: " + - error, - ); + // Function to check if option types include the specified type + const includesType = (option) => + option.types && option.types.includes(type); + + // Get the list of options for a user to select from based on type, if there + // were none that matched based on field names + if (!options || !options.length) { + options = this.operatorOptions.filter(includesType); } + + return options; }, /** * getSelectedOperator - Based on values set on the model, get the label to show * in the "operator" filed of the Query Rule - * - * @return {string} The operator label + * @returns {string} The operator label */ - getSelectedOperator: function () { - try { - // This view - var view = this, - // The options that we will filter down - options = this.operatorOptions, - // The user-facing operator label that we will return - selectedOperator = ""; - - // --- Filter 1 - Filter options by type --- // - - // Reduce list of options to only those that apply to the current filter type - var type = view.model.get("nodeName"); - var options = this.getOperatorOptions(); - - // --- Filter 2 - filter by 'matchSubstring', 'exclude', 'min', 'max' --- // - - // Create the conditions based on the model - var conditions = _.pick( - this.model.attributes, - "matchSubstring", - "exclude", - "min", - "max", - ); + getSelectedOperator() { + // This view + const view = this; + // The user-facing operator label that we will return + let selectedOperator = ""; + + // --- Filter 1 - Filter options by type --- // + + // Reduce list of options to only those that apply to the current filter type + const type = view.model.get("nodeName"); + let operatorOptions = this.getOperatorOptions(); + + // --- Filter 2 - filter by 'matchSubstring', 'exclude', 'min', 'max' --- // + + // Create the conditions based on the model + const conditions = _.pick( + this.model.attributes, + "matchSubstring", + "exclude", + "min", + "max", + ); - var isNumeric = ["dateFilter", "numericFilter"].includes(type); + const isNumeric = ["dateFilter", "numericFilter"].includes(type); - if (!conditions.min && conditions.min !== 0) { - if (isNumeric) { - conditions.hasMin = false; - } - } else if (isNumeric) { - conditions.hasMin = true; + if (!conditions.min && conditions.min !== 0) { + if (isNumeric) { + conditions.hasMin = false; } - if (!conditions.max && conditions.max !== 0) { - if (isNumeric) { - conditions.hasMax = false; - } - } else if (isNumeric) { - conditions.hasMax = true; + } else if (isNumeric) { + conditions.hasMin = true; + } + if (!conditions.max && conditions.max !== 0) { + if (isNumeric) { + conditions.hasMax = false; } + } else if (isNumeric) { + conditions.hasMax = true; + } - delete conditions.min; - delete conditions.max; + delete conditions.min; + delete conditions.max; - var options = _.where(options, conditions); + operatorOptions = _.where(operatorOptions, conditions); - // --- Filter 3 - filter based on the value, if there's > 1 option --- // + // --- Filter 3 - filter based on the value, if there's > 1 option --- // - if (options.length > 1) { - // Model values that determine the user-facing operator eg ["*"], [true], - // [false] - var specialValues = _.compact( - _.pluck(this.operatorOptions, "values"), - ), - specialValues = specialValues.map((val) => JSON.stringify(val)), - specialValues = _.uniq(specialValues); + if (operatorOptions.length > 1) { + // Model values that determine the user-facing operator eg ["*"], [true], + // [false] + let specialValues = _.compact( + _.pluck(this.operatorOptions, "values"), + ); + specialValues = specialValues.map((val) => JSON.stringify(val)); + specialValues = _.uniq(specialValues); - options = options.filter(function (option) { - var modelValsStringified = JSON.stringify( - view.model.get("values"), - ); - if (specialValues.includes(modelValsStringified)) { - if (JSON.stringify(option.values) === modelValsStringified) { - return true; - } - } else { - if (!option.values) { - return true; - } - } - }); - } - // --- Return value --- // + const modelValues = view.model.get("values"); + const modelValuesString = JSON.stringify(modelValues); - if (options.length === 1) { - selectedOperator = options[0].label; - } + operatorOptions = operatorOptions.filter((option) => { + if (!option.values) return true; // Filter in options without values + if (!specialValues.includes(modelValuesString)) return false; // If model values not special, filter out + return JSON.stringify(option.values) === modelValuesString; // Check exact match for special values + }); + } + // --- Return value --- // - return selectedOperator; - } catch (e) { - console.error( - "Failed to select an operator in the Query Rule View, error" + - " message: " + - e, - ); + if (operatorOptions.length === 1) { + selectedOperator = operatorOptions[0].label; } + + return selectedOperator; }, /** * getCategory - Given an array of query fields, get the user-facing category that * these fields belong to. If there are fields from multiple categories, then a * default "Text" category is returned. - * * @param {string[]} fields An array of query (Solr) fields - * @return {string} The label for the category that the given fields belong to + * @returns {string} The label for the category that the given fields belong to */ - getCategory: function (fields) { - try { - var categories = [], - // When fields is empty or are different types - defaultCategory = "Text"; + getCategory(fields) { + const categories = []; + // When fields is empty or are different types + const defaultCategory = "Text"; - if (!fields || fields.length === 0 || fields[0] === "") { - return defaultCategory; - } + if (!fields || fields.length === 0 || fields[0] === "") { + return defaultCategory; + } - fields.forEach((field, i) => { - // Get the category of the field from the matching filter model in the Query - // Fields Collection - var fieldModel = MetacatUI.queryFields.findWhere({ name: field }); - categories.push(fieldModel.get("category")); - }); + fields.forEach((field) => { + // Get the category of the field from the matching filter model in the Query + // Fields Collection + const fieldModel = MetacatUI.queryFields.findWhere({ name: field }); + categories.push(fieldModel.get("category")); + }); - // Test of all the fields are of the same type - var allEqual = categories.every((val, i, arr) => val === arr[0]); + // Test of all the fields are of the same type + const allEqual = categories.every((val, i, arr) => val === arr[0]); - if (allEqual) { - return categories[0]; - } else { - return defaultCategory; - } - } catch (e) { - console.log( - "Failed to detect the category for a group of filters in the" + - " Query Rule View, error message: " + - e, - ); + if (allEqual) { + return categories[0]; } + return defaultCategory; }, /** * Create and insert an input field where the user can provide a search value */ - addValueSelect: function () { - try { - var view = this; - (fields = this.model.get("fields")), - (filterType = MetacatUI.queryFields.getRequiredFilterType(fields)), - (category = this.getCategory(fields)), - (interfaces = this.valueSelectUImap), - (label = ""); - - // To help guide users to create valid queries, the type of value field will - // vary based on the type of field (i.e. filter nodeName), and the operator - // selected. - - // Some user-facing operators (e.g. "is true") don't require a value to be set - var selectedOperator = _.findWhere(this.operatorOptions, { - label: this.getSelectedOperator(), - }); - if (selectedOperator) { - if (selectedOperator.values && selectedOperator.values.length) { - return; - } + addValueSelect() { + const view = this; + const fields = this.model.get("fields"); + const filterType = MetacatUI.queryFields.getRequiredFilterType(fields); + const category = this.getCategory(fields); + const interfaces = this.valueSelectUImap; + let label = ""; + + // To help guide users to create valid queries, the type of value field will + // vary based on the type of field (i.e. filter nodeName), and the operator + // selected. + + // Some user-facing operators (e.g. "is true") don't require a value to be set + const selectedOperator = _.findWhere(this.operatorOptions, { + label: this.getSelectedOperator(), + }); + if (selectedOperator) { + if (selectedOperator.values && selectedOperator.values.length) { + return; } + } - // Find the appropriate UI to use the the value select field. Find the first - // match in the valueSelectUImap according to the filter type and the - // categories associated with the metadata field. - var interfaceProperties = _.find( - interfaces, - function (thisInterface) { - var typesMatch = true, - categoriesMatch = true, - namesMatch = true; - if ( - thisInterface.queryFields && - thisInterface.queryFields.length - ) { - fields.forEach((field, i) => { - if (thisInterface.queryFields.includes(field) === false) { - namesMatch = false; - } - }); - } - if ( - thisInterface.filterTypes && - thisInterface.filterTypes.length - ) { - typesMatch = thisInterface.filterTypes.includes(filterType); + // Find the appropriate UI to use the the value select field. Find the first + // match in the valueSelectUImap according to the filter type and the + // categories associated with the metadata field. + const interfaceProperties = _.find(interfaces, (thisInterface) => { + let typesMatch = true; + let categoriesMatch = true; + let namesMatch = true; + if (thisInterface.queryFields && thisInterface.queryFields.length) { + fields.forEach((field) => { + if (thisInterface.queryFields.includes(field) === false) { + namesMatch = false; } - if (thisInterface.categories && thisInterface.categories.length) { - categoriesMatch = thisInterface.categories.includes(category); - } - return typesMatch && categoriesMatch && namesMatch; - }, - ); - - this.valueSelect = interfaceProperties.uiFunction.call(this); - if (interfaceProperties.label && interfaceProperties.label.length) { - label = $( - "

" + - interfaceProperties.label + - "

", - ); + }); } - - // Append and render the chosen value selector - this.el.append(view.valueSelect.el); - this.valueSelect.$el.addClass(this.valuesClass); - view.valueSelect.render(); - if (label) { - view.valueSelect.$el.prepend(label); + if (thisInterface.filterTypes && thisInterface.filterTypes.length) { + typesMatch = thisInterface.filterTypes.includes(filterType); } + if (thisInterface.categories && thisInterface.categories.length) { + categoriesMatch = thisInterface.categories.includes(category); + } + return typesMatch && categoriesMatch && namesMatch; + }); - // Make sure the listeners set below are not set multiple times - this.stopListening( - view.valueSelect, - "changeSelection inputFocus separatorChanged", + this.valueSelect = interfaceProperties.uiFunction.call(this); + if (interfaceProperties.label && interfaceProperties.label.length) { + label = $( + `

${interfaceProperties.label}

`, ); + } - // Update model when the values change - note that the date & numeric filter - // views do not trigger a 'changeSelection' event, (because they are not based - // on a SearchSelect View) but update the models directly - this.listenTo( - view.valueSelect, - "changeSelection", - this.handleValueChange, - ); + // Append and render the chosen value selector + this.el.append(view.valueSelect.el); + this.valueSelect.$el.addClass(this.valuesClass); + view.valueSelect.render(); + if (label) { + view.valueSelect.$el.prepend(label); + } - // Update the model when the operator changes - this.listenTo( - view.valueSelect, - "separatorChanged", - function (newOperator) { - this.model.set("operator", newOperator); - }, + // Make sure the listeners set below are not set multiple times + this.stopListening( + view.valueSelect, + "changeSelection inputFocus separatorChanged", + ); + + // Update model when the values change - note that the date & numeric filter + // views do not trigger a 'changeSelection' event, (because they are not based + // on a SearchSelect View) but update the models directly + this.listenTo( + view.valueSelect, + "changeSelection", + this.handleValueChange, + ); + + // Update the model when the operator changes + this.listenTo(view.valueSelect, "separatorChanged", (newOperator) => { + this.model.set("operator", newOperator); + }); + + // Show a message that reminds the user that capitalization matters when they + // are typing a value for a field that is case-sensitive. + this.listenTo(view.valueSelect, "inputFocus", () => { + const currentFields = this.model.get("fields"); + const isCaseSensitive = _.some(currentFields, (field) => + MetacatUI.queryFields.findWhere({ + name: field, + caseSensitive: true, + }), ); - // Show a message that reminds the user that capitalization matters when they - // are typing a value for a field that is case-sensitive. - this.listenTo(view.valueSelect, "inputFocus", function (event) { - var fields = this.model.get("fields"); - var isCaseSensitive = _.some(fields, function (field) { - return MetacatUI.queryFields.findWhere({ - name: field, - caseSensitive: true, - }); - }); - if (isCaseSensitive) { - var fieldsText = "The field"; - if (fields.length > 1) { - fieldsText = "At least one of the fields"; - } - var message = - " Hint: " + - fieldsText + - " you selected is case-sensitive. Capitalization matters here."; - view.valueSelect.showMessage( - message, - (type = "info"), - (removeOnChange = false), - ); - } else { - view.valueSelect.removeMessages(); + if (isCaseSensitive) { + let fieldsText = "The field"; + if (currentFields.length > 1) { + fieldsText = "At least one of the fields"; } - }); - // Set the value to the value provided if there was one. Then validateValue() - } catch (e) { - console.error( - "Error adding a search value input in the Query Rule View," + - " error message:", - e, - ); - } + const message = ` Hint: ${fieldsText} you selected is case-sensitive. Capitalization matters here.`; + view.valueSelect.showMessage(message, "info", false); + } else { + view.valueSelect.removeMessages(); + } + }); }, /** * handleValueChange - Called when the select values for rule are changed. Updates * the model. - * * @param {string[]} newValues The new values that were selected */ - handleValueChange: function (newValues) { - try { - // TODO: validate values - - // Don't add empty values to the model - newValues = _.reject(newValues, function (val) { - return val === ""; - }); - this.model.set("values", newValues); - } catch (e) { - console.error( - "Failed to handle a change in select values in the Query Ryle" + - " View, error message: " + - e, - ); - } + handleValueChange(newValues) { + // TODO: validate values + // Don't add empty values to the model + const filteredValues = _.reject(newValues, (val) => val === ""); + this.model.set("values", filteredValues); }, - // /** - // * Ensure the value entered is valid, given the metadata field selected. - // * If it's not, show an error. If it is, remove the error if there was one. - // * - // * @return {type} description - // */ - // validateValue: function() {// TODO - // }, - /** * Remove one of the three input fields from the rule - * * @param {string} inputType Which of the inputs to remove? "field", "operator", * or "value" */ - removeInput: function (inputType) { - try { - // TODO - what, if any, model updates should happen here? - switch (inputType) { - case "value": - if (this.valueSelect) { - this.stopListening( - this.valueSelect, - "changeSelection inputFocus", - ); - this.valueSelect.remove(); - this.valueSelect = null; - } - break; - case "operator": - if (this.operatorSelect) { - this.stopListening(this.operatorSelect, "changeSelection"); - this.operatorSelect.remove(); - this.operatorSelect = null; - } - break; - case "field": - if (this.fieldSelect) { - this.stopListening(this.fieldSelect, "changeSelection"); - this.fieldSelect.remove(); - this.fieldSelect = null; - } - break; - default: - console.error( - "Must specify either value, operator, or field in the" + - " removeInput function in the Query Rule View", + removeInput(inputType) { + // TODO - what, if any, model updates should happen here? + switch (inputType) { + case "value": + if (this.valueSelect) { + this.stopListening( + this.valueSelect, + "changeSelection inputFocus", ); - } - } catch (e) { - console.error( - "Error removing an input from the Query Rule View, error" + - " message:", - e, - ); + this.valueSelect.remove(); + this.valueSelect = null; + } + break; + case "operator": + if (this.operatorSelect) { + this.stopListening(this.operatorSelect, "changeSelection"); + this.operatorSelect.remove(); + this.operatorSelect = null; + } + break; + case "field": + if (this.fieldSelect) { + this.stopListening(this.fieldSelect, "changeSelection"); + this.fieldSelect.remove(); + this.fieldSelect = null; + } + break; + default: + break; } }, /** * Indicate to the user that the rule will be removed when they hover over the * remove button. + * @param {Event} e The mouseover or mouseout event */ - previewRemove: function (e) { - try { - var normalOpacity = 1.0, - previewOpacity = 0.2, - speed = 175; + previewRemove(e) { + const normalOpacity = 1.0; + const previewOpacity = 0.2; + const speed = 175; - var removeEl = e.target; - var subElements = this.$el.children().not(removeEl); + const removeEl = e.target; + const subElements = this.$el.children().not(removeEl); - if (e.type === "mouseover") { - subElements.fadeTo(speed, previewOpacity); - $(removeEl).fadeTo(speed, normalOpacity); - } - if (e.type === "mouseout") { - subElements.fadeTo(speed, normalOpacity); - $(removeEl).fadeTo(speed, previewOpacity); - } - } catch (error) { - console.log( - "Error showing a preview of the removal of a Query Rule View," + - " details: " + - error, - ); + if (e.type === "mouseover") { + subElements.fadeTo(speed, previewOpacity); + $(removeEl).fadeTo(speed, normalOpacity); + } + if (e.type === "mouseout") { + subElements.fadeTo(speed, normalOpacity); + $(removeEl).fadeTo(speed, previewOpacity); } }, @@ -1798,18 +1526,13 @@ define([ * removeSelf - When the delete button is clicked, remove this entire View and * associated model */ - removeSelf: function () { - try { - $("body .popover").remove(); - $("body .tooltip").remove(); - if (this.model && this.model.collection) { - this.model.collection.remove(this.model); - } - this.remove(); - } catch (error) { - console.log("Error removing a Query Rule View, details: " + error); + removeSelf() { + $("body .popover").remove(); + $("body .tooltip").remove(); + if (this.model && this.model.collection) { + this.model.collection.remove(this.model); } + this.remove(); }, }, - ); -}); + )); From 53b6611082d60beca4d63de3348c0a3a9efb220b Mon Sep 17 00:00:00 2001 From: Matthew B <106352182+artntek@users.noreply.github.com> Date: Tue, 2 Jul 2024 16:36:11 -0700 Subject: [PATCH 059/169] add metadata to enable helm push to ghcr --- helm/Chart.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 54f9fdf34..5a72c91b6 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -1,6 +1,12 @@ apiVersion: v2 name: metacatui -description: MetacatUI, a web interface for DataONE repositories +description: | + Helm chart for Kubernetes Deployment of MetacatUI, a web interface for DataONE repositories + (https://github.com/NCEAS/metacatui) + +# OCI Annotations - see https://github.com/helm/helm/pull/11204 +sources: + - https://github.com/NCEAS/metacatui # A chart can be either an 'application' or a 'library' chart. # From 1bd8bb8d7ad762ab562465cf27ef2bf0d6a25ea9 Mon Sep 17 00:00:00 2001 From: robyngit Date: Wed, 3 Jul 2024 10:52:27 -0400 Subject: [PATCH 060/169] Fix linting errors in SearchableSelectView Issue #2253 --- src/js/app.js | 1 + .../searchSelect/SearchableSelectView.js | 1391 ++++++++--------- 2 files changed, 612 insertions(+), 780 deletions(-) diff --git a/src/js/app.js b/src/js/app.js index 45cd25287..324b55c68 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -94,6 +94,7 @@ require.config({ semanticUItransition: MetacatUI.root + "/components/semanticUI/transition.min", semanticUIdropdown: MetacatUI.root + "/components/semanticUI/dropdown.min", + semanticAPI: `${MetacatUI.root}/components/semanticUI/api.min`, // To make elements drag and drop, sortable sortable: MetacatUI.root + "/components/sortable.min", //Cesium diff --git a/src/js/views/searchSelect/SearchableSelectView.js b/src/js/views/searchSelect/SearchableSelectView.js index 955d99a85..37eaba067 100644 --- a/src/js/views/searchSelect/SearchableSelectView.js +++ b/src/js/views/searchSelect/SearchableSelectView.js @@ -3,11 +3,11 @@ define([ "underscore", "backbone", "semanticUItransition", - "text!" + MetacatUI.root + "/components/semanticUI/transition.min.css", + `text!${MetacatUI.root}/components/semanticUI/transition.min.css`, "semanticUIdropdown", - "text!" + MetacatUI.root + "/components/semanticUI/dropdown.min.css", + `text!${MetacatUI.root}/components/semanticUI/dropdown.min.css`, "text!templates/selectUI/searchableSelect.html", -], function ( +], ( $, _, Backbone, @@ -16,19 +16,19 @@ define([ Dropdown, DropdownCSS, Template, -) { +) => /** * @class SearchableSelectView * @classdesc A select interface that allows the user to search from within * the options, and optionally select multiple items. Also allows the items * to be grouped, and to display an icon or image for each item. * @classcategory Views/SearchSelect - * @extends Backbone.View - * @constructor + * @augments Backbone.View + * @class * @since 2.14.0 * @screenshot views/searchSelect/SearchableSelectView.png */ - return Backbone.View.extend( + Backbone.View.extend( /** @lends SearchableSelectView.prototype */ { /** @@ -173,8 +173,7 @@ define([ * single option. To create category headings, provide an object containing named * objects, where the key for each object is the category title to display, and * the value of each object comprises the option properties. - * @name SearchableSelectView#options - * @type {Object[]|Object} + * @typedef {object[] | object} SearchableSelectOptions * @property {string} icon - The name of a Font Awesome 3.2.1 icon to display to * the left of the label (e.g. "lemon", "heart") * @property {string} image - The complete path to an image to use instead of an @@ -229,6 +228,11 @@ define([ * ] * } */ + + /** + * The options that a user can select from in the dropdown menu. + * @type {SearchableSelectOptions} + */ options: [], /** @@ -244,7 +248,7 @@ define([ * menu content from an API endpoint. Details of what can be set here are * specified by the Semantic-UI / Fomantic-UI package. Set to false if not * retrieving remote content. - * @type {Object|booealn} + * @type {object | booealn} * @default false * @since 2.15.0 * @see {@link https://fomantic-ui.com/modules/dropdown.html#remote-settings} @@ -262,212 +266,192 @@ define([ /** * Creates a new SearchableSelectView - * @param {Object} options - A literal object with options to pass to the view + * @param {object} opts - A literal object with options to pass to the view */ - initialize: function (options) { - try { - // Add CSS required for this view - MetacatUI.appModel.addCSS(TransitionCSS, "semanticUItransition"); - MetacatUI.appModel.addCSS(DropdownCSS, "semanticUIdropdown"); - - // If pre-selected values that are passed to this view are also attached to a - // model (e.g. when they were passed to this view as {selected: - // parentView.model.get("values")}), then it's important that we use a clone - // instead. Otherwise this view may silently update the model, and important - // events may not be triggered. - if (options.selected) { - options.selected = _.clone(options.selected); - } - - // If pre-selected values that are passed to this view are also attached to a - // model (e.g. when they were passed to this view as {selected: - // parentView.model.get("values")}), then it's important that we use a clone - // instead. Otherwise this view may silently update the model, and important - // events may not be triggered. - if (options.selected) { - options.selected = _.clone(options.selected); - } + initialize(opts) { + const options = opts || {}; + + // Add CSS required for this view + MetacatUI.appModel.addCSS(TransitionCSS, "semanticUItransition"); + MetacatUI.appModel.addCSS(DropdownCSS, "semanticUIdropdown"); + + // If pre-selected values that are passed to this view are also attached to a + // model (e.g. when they were passed to this view as {selected: + // parentView.model.get("values")}), then it's important that we use a clone + // instead. Otherwise this view may silently update the model, and important + // events may not be triggered. + if (options.selected) { + options.selected = _.clone(options.selected); + } - // Get all the options and apply them to this view - if (typeof options == "object") { - var optionKeys = Object.keys(options); - _.each( - optionKeys, - function (key, i) { - this[key] = options[key]; - }, - this, - ); - } - } catch (e) { - console.log( - "Failed to initialize a Searchable Select view, error message:", - e, - ); + // If pre-selected values that are passed to this view are also attached to a + // model (e.g. when they were passed to this view as {selected: + // parentView.model.get("values")}), then it's important that we use a clone + // instead. Otherwise this view may silently update the model, and important + // events may not be triggered. + if (options.selected) { + options.selected = _.clone(options.selected); } + + // Get all the options and apply them to this view + _.extend(this, options); }, /** * Render the view - * - * @return {SearchableSelect} Returns the view + * @returns {SearchableSelect} Returns the view */ - render: function () { - try { - var view = this; - - if (view.apiSettings && !view.semanticAPILoaded) { - require([ - MetacatUI.root + "/components/semanticUI/api.min.js", - ], function (SemanticAPI) { - view.semanticAPILoaded = true; - view.render(); - }); - return; - } + render() { + const view = this; + + if (view.apiSettings && !view.semanticAPILoaded) { + // eslint-disable-next-line import/no-dynamic-require + require(["semanticAPI"], (_SemanticAPI) => { + view.semanticAPILoaded = true; + view.render(); + }); + return this; + } - // Render the template using the view attributes - this.$el.html(this.template(this)); - - // Start the dropdown in a disabled state. - // This allows us to pre-select values without triggering a change - // event. - this.disable(); - this.showLoading(); - - // Initialize the dropdown interface - // For explanations of settings, see: - // https://semantic-ui.com/modules/dropdown.html#/settings - this.$selectUI = this.$el.find(".ui.dropdown").dropdown({ - keys: { - // So that a user may enter search text using a comma - delimiter: false, - }, - apiSettings: this.apiSettings, - fullTextSearch: true, - duration: 90, - forceSelection: false, - ignoreDiacritics: true, - clearable: view.clearable, - allowAdditions: view.allowAdditions, - hideAdditions: false, - allowReselection: true, - onRemove: function (removedValue) { - // Callback when a value is removed *for multi-select inputs only* - // Remove the value from the selected array - view.selected = view.selected.filter(function (value) { - return value !== removedValue; - }); - }, - onLabelCreate: function (value, text) { - // Callback when a label is created *for multi-select inputs only* - - // Add the value to the selected array (but don't add twice). Do this in - // the onLabelCreate callback instead of in the onAdd callback because - // we would like to update the selected array before we create the - // separator element (below). - if (!view.selected.includes(value)) { - view.selected.push(value); - } - // Add a separator between labels if required. - var label = this; - if (view.separatorRequired.call(view)) { - // Create the separator element. - var separator = view.createSeparator.call(view); - if (separator) { - // Attach the separator to the label so that we can easily remove it - // when the label is removed. - label.data("separator", separator); - // Add it before the label element. - label = separator.add(label); - } - } - return label; - }, - onLabelRemove(value) { - // Call back when a user deletes a label *for multi-select inputs only* - var label = this; - // Remove the separator before this label if there is one. - var sep = label.data("separator"); - if (sep) { - sep.remove(); - } - // If this is the first label in an input of at least two, then delete - // the separator directly *after* this label - The label that's second - // will become first, and should not have an separator before it. - var allLabels = view.$selectUI.find(".label"); - if (allLabels.index(label) === 0) { - var separatorAfter = label.next("." + view.separatorClass); - if (separatorAfter) { - separatorAfter.remove(); - } + // Render the template using the view attributes + this.$el.html(this.template(this)); + + // Start the dropdown in a disabled state. + // This allows us to pre-select values without triggering a change + // event. + this.disable(); + this.showLoading(); + + // Initialize the dropdown interface + // For explanations of settings, see: + // https://semantic-ui.com/modules/dropdown.html#/settings + this.$selectUI = this.$el.find(".ui.dropdown").dropdown({ + keys: { + // So that a user may enter search text using a comma + delimiter: false, + }, + apiSettings: this.apiSettings, + fullTextSearch: true, + duration: 90, + forceSelection: false, + ignoreDiacritics: true, + clearable: view.clearable, + allowAdditions: view.allowAdditions, + hideAdditions: false, + allowReselection: true, + onRemove(removedValue) { + // Callback when a value is removed *for multi-select inputs only* + // Remove the value from the selected array + view.selected = view.selected.filter( + (value) => value !== removedValue, + ); + }, + onLabelCreate(value, _text) { + // Callback when a label is created *for multi-select inputs only* + + // Add the value to the selected array (but don't add twice). Do this in + // the onLabelCreate callback instead of in the onAdd callback because + // we would like to update the selected array before we create the + // separator element (below). + if (!view.selected.includes(value)) { + view.selected.push(value); + } + // Add a separator between labels if required. + let label = this; + if (view.separatorRequired.call(view)) { + // Create the separator element. + const separator = view.createSeparator.call(view); + if (separator) { + // Attach the separator to the label so that we can easily remove it + // when the label is removed. + label.data("separator", separator); + // Add it before the label element. + label = separator.add(label); } - }, - onChange: function (values, text, $choice) { - // Callback when values change for any type of input. - - // NOTE: The "values" argument is a string that contains all the - // selected values separated by commas. We updated the view.selected - // array with the onLabelCreate and onRemove callbacks instead of using - // the values argument passed to this function in order to allow commas - // within individual values. For example, if the user selected the value - // "x" and the value "y,z", the values string would be "x,y,z" and it - // would be difficult to see that two values were selected instead of - // three. - - // Update values for single-select inputs (multi-select are updated - // using the onLabelCreate and onRemove callbacks) - if (!view.allowMulti) { - view.selected = [values]; + } + return label; + }, + onLabelRemove(_value) { + // Call back when a user deletes a label *for multi-select inputs only* + const label = this; + // Remove the separator before this label if there is one. + const sep = label.data("separator"); + if (sep) { + sep.remove(); + } + // If this is the first label in an input of at least two, then delete + // the separator directly *after* this label - The label that's second + // will become first, and should not have an separator before it. + const allLabels = view.$selectUI.find(".label"); + if (allLabels.index(label) === 0) { + const separatorAfter = label.next(`.${view.separatorClass}`); + if (separatorAfter) { + separatorAfter.remove(); } + } + }, + onChange(values, _text, _$choice) { + // Callback when values change for any type of input. + + // NOTE: The "values" argument is a string that contains all the + // selected values separated by commas. We updated the view.selected + // array with the onLabelCreate and onRemove callbacks instead of using + // the values argument passed to this function in order to allow commas + // within individual values. For example, if the user selected the value + // "x" and the value "y,z", the values string would be "x,y,z" and it + // would be difficult to see that two values were selected instead of + // three. + + // Update values for single-select inputs (multi-select are updated + // using the onLabelCreate and onRemove callbacks) + if (!view.allowMulti) { + view.selected = [values]; + } - // Trigger an event if items are selected after the UI has been rendered - // (It is set as disabled until fully rendered). - if (!$(this).hasClass("disabled")) { - var newValues = _.clone(view.selected); - view.trigger("changeSelection", newValues); - } + // Trigger an event if items are selected after the UI has been rendered + // (It is set as disabled until fully rendered). + if (!$(this).hasClass("disabled")) { + const newValues = _.clone(view.selected); + view.trigger("changeSelection", newValues); + } - // Refresh the tooltips on the labels/text + // Refresh the tooltips on the labels/text - // Ensure tooltips for labels are removed - $(".search-select-tooltip").remove(); + // Ensure tooltips for labels are removed + $(".search-select-tooltip").remove(); - // Add a tooltip for single select elements (.text) or multi-select - // elements (.label). Delay so that to give time for DOM elements to be - // added or removed. - setTimeout(function (params) { - var textEl = view.$selectUI.find(".text:not(.default),.label"); - // Single select text element will not have the value attribute, add - // it so that we can find the matching description for the tooltip - if (!textEl.data("value") && !view.allowMulti) { - textEl.data("value", values); - } - if (textEl) { - textEl.each(function (i, el) { - view.addTooltip.call(view, el, "top"); - }); - } - }, 50); - }, - }); + // Add a tooltip for single select elements (.text) or multi-select + // elements (.label). Delay so that to give time for DOM elements to be + // added or removed. + setTimeout(() => { + const textEl = view.$selectUI.find(".text:not(.default),.label"); + // Single select text element will not have the value attribute, add + // it so that we can find the matching description for the tooltip + if (!textEl.data("value") && !view.allowMulti) { + textEl.data("value", values); + } + if (textEl) { + textEl.each((i, el) => { + view.addTooltip.call(view, el, "top"); + }); + } + }, 50); + }, + }); - view.$selectUI.data("view", view); + view.$selectUI.data("view", view); - view.postRender(); + view.postRender(); - return this; - } catch (e) { - console.log("Error rendering the search select, error message: ", e); - } + return this; }, /** * Change the options available in the dropdown menu and re-render. - * @param {SearchableSelectView#options} options - The new options + * @param {SearchableSelectOptions} options - The new options * @since 2.24.0 */ - updateOptions: function (options) { + updateOptions(options) { this.options = options; this.render(); }, @@ -475,110 +459,92 @@ define([ /** * Checks whether a separator should be created for the label that was just * created, but not yet attached to the DOM - * @return {boolean} - Returns true if a separator should be created, false + * @returns {boolean} - Returns true if a separator should be created, false * otherwise. * @since 2.15.0 */ - separatorRequired: function () { - try { - if ( - // Separators not required if only one selection is allowed - !this.allowMulti || - // Need separator text to create a separator element - !this.separatorText || - // Need the list of selected values to determine the value's position - !this.selected || - // Separator is only required between two or more values - this.selected.length <= 1 || - // Separator is only required after the first element has been added - this.$selectUI.find(".label").length === 0 - ) { - return false; - } else { - return true; - } - } catch (error) { - console.log( - "Error checking if a label in a searchable select input " + - "requires a separator. Assuming that it does not need one. Error details: " + - error, - ); + separatorRequired() { + if ( + // Separators not required if only one selection is allowed + !this.allowMulti || + // Need separator text to create a separator element + !this.separatorText || + // Need the list of selected values to determine the value's position + !this.selected || + // Separator is only required between two or more values + this.selected.length <= 1 || + // Separator is only required after the first element has been added + this.$selectUI.find(".label").length === 0 + ) { return false; } + return true; }, /** * Create the HTML for a separator element to insert between two labels. The * view.separatorClass is added to the separator element. - * @return {JQuery} Returns the separator as a jQuery element + * @returns {JQuery} Returns the separator as a jQuery element * @since 2.15.0 */ - createSeparator: function () { - try { - var view = this; - var separatorText = this.separatorText; - // Text is required to create a separator. - if (!separatorText) { - return null; - } - var separator = $("" + separatorText + ""); - separator.addClass(this.separatorClass); - - // Set a listener to change the text to one of the separatorText - // options on click, and to highlight all the separators when one is hovered - var separatorElHovered = false; - if (view.separatorTextOptions && view.separatorTextOptions.length) { - // Indicate that the separator is clickable - separator.css("cursor", "pointer"); - // Make sure the listeners set below are only set once - separator.off("click mouseenter mouseout"); - // Change all the separator text when one is clicked - separator.on("click", function () { - view.changeSeparator(); - }); - // Create the tooltip - if (view.changeableSeparatorTooltip) { - $(separator).tooltip("destroy"); - $(separator).tooltip({ - title: view.changeableSeparatorTooltip, - trigger: "manual", - }); - } - // Highlight all of the separator elements when one is hovered - separator.on("mouseenter", function () { - var separatorEls = view.$el.find("." + view.separatorClass); - separatorElHovered = true; - // Add a delay before the highlight class is added - setTimeout(function () { - if (separatorElHovered) { - separatorEls.addClass(view.changeableSeparatorClass); - if (view.changeableSeparatorTooltip) { - // Add an even longer delay before the tooltip is shown - setTimeout(function () { - if (separatorElHovered) { - $(separator).tooltip("show"); - } - }, 600); - } - } - }, 285); - }); - // Hide all the tooltips and remove the highlight class on mouse out - separator.on("mouseout", function () { - separatorElHovered = false; - var separatorEls = view.$el.find("." + view.separatorClass); - separatorEls.removeClass(view.changeableSeparatorClass); - separatorEls.tooltip("hide"); + createSeparator() { + const view = this; + const { separatorText } = this; + // Text is required to create a separator. + if (!separatorText) { + return null; + } + const separator = $(`${separatorText}`); + separator.addClass(this.separatorClass); + + // Set a listener to change the text to one of the separatorText + // options on click, and to highlight all the separators when one is hovered + let separatorElHovered = false; + if (view.separatorTextOptions && view.separatorTextOptions.length) { + // Indicate that the separator is clickable + separator.css("cursor", "pointer"); + // Make sure the listeners set below are only set once + separator.off("click mouseenter mouseout"); + // Change all the separator text when one is clicked + separator.on("click", () => { + view.changeSeparator(); + }); + // Create the tooltip + if (view.changeableSeparatorTooltip) { + $(separator).tooltip("destroy"); + $(separator).tooltip({ + title: view.changeableSeparatorTooltip, + trigger: "manual", }); } - return separator; - } catch (error) { - console.log( - "There was an error creating a separator element in a " + - "Searchable Select View. Error details: " + - error, - ); + // Highlight all of the separator elements when one is hovered + separator.on("mouseenter", () => { + const separatorEls = view.$el.find(`.${view.separatorClass}`); + separatorElHovered = true; + // Add a delay before the highlight class is added + setTimeout(() => { + if (separatorElHovered) { + separatorEls.addClass(view.changeableSeparatorClass); + if (view.changeableSeparatorTooltip) { + // Add an even longer delay before the tooltip is shown + setTimeout(() => { + if (separatorElHovered) { + $(separator).tooltip("show"); + } + }, 600); + } + } + }, 285); + }); + // Hide all the tooltips and remove the highlight class on mouse out + separator.on("mouseout", () => { + separatorElHovered = false; + const separatorEls = view.$el.find(`.${view.separatorClass}`); + separatorEls.removeClass(view.changeableSeparatorClass); + separatorEls.tooltip("hide"); + }); } + return separator; }, /** @@ -586,581 +552,471 @@ define([ * set in the {@link SearchableSelectView#separatorTextOptions}. Triggers a * "separatorChanged" event that passes on the new separator value. */ - changeSeparator: function () { - try { - var view = this; - if ( - !view.separatorTextOptions || - !view.separatorTextOptions.length || - !view.separatorText - ) { - return; - } - // Get the next separator text option - var currentIndex = view.separatorTextOptions.indexOf( - view.separatorText, - ), - nextIndex = currentIndex + 1; - if (currentIndex === -1 || !view.separatorTextOptions[nextIndex]) { - nextIndex = 0; - } - // Update the current separator text on the view - view.separatorText = view.separatorTextOptions[nextIndex]; - // Change the separator text for all of the separators in the view with an - // animation - var separatorEls = view.$el.find("." + view.separatorClass); - separatorEls.transition({ - animation: "pulse", - displayType: "inline-block", - duration: "250ms", - onComplete: function () { - $(this).text(view.separatorText); - }, - }); - // Trigger an event for parent views - view.trigger("separatorChanged", view.separatorText); - } catch (error) { - console.log( - "There was an error switching the separator text in a SearchableSelectView" + - ". Error details: " + - error, - ); + changeSeparator() { + const view = this; + if ( + !view.separatorTextOptions || + !view.separatorTextOptions.length || + !view.separatorText + ) { + return; } + // Get the next separator text option + const currentIndex = view.separatorTextOptions.indexOf( + view.separatorText, + ); + let nextIndex = currentIndex + 1; + if (currentIndex === -1 || !view.separatorTextOptions[nextIndex]) { + nextIndex = 0; + } + // Update the current separator text on the view + view.separatorText = view.separatorTextOptions[nextIndex]; + // Change the separator text for all of the separators in the view with an + // animation + const separatorEls = view.$el.find(`.${view.separatorClass}`); + separatorEls.transition({ + animation: "pulse", + displayType: "inline-block", + duration: "250ms", + onComplete() { + $(this).text(view.separatorText); + }, + }); + // Trigger an event for parent views + view.trigger("separatorChanged", view.separatorText); }, /** * updateMenu - Re-render the menu of options. Useful after changing * the options that are set on the view. */ - updateMenu: function () { - try { - var menu = $(this.template(this).trim()).find(".menu")[0].innerHTML; - this.$el.find(".menu").html(menu); - } catch (e) { - console.log( - "Failed to update a searchable select menu, error message: " + e, - ); - } + updateMenu() { + const menu = $(this.template(this).trim()).find(".menu")[0].innerHTML; + this.$el.find(".menu").html(menu); }, /** * postRender - Updates to the view once the dropdown UI has loaded */ - postRender: function () { - try { - var view = this; - view.trigger("postRender"); - - // Add tool tips for the description - this.$el.find(".item").each(function () { - view.addTooltip(this); - }); - - // Show an error message if the pre-selected options are not in the - // list of available options (only if user additions are not allowed) - if (!view.allowAdditions) { - if (view.selected && view.selected.length) { - var invalidOptions = []; - view.selected.forEach(function (item) { - if (!view.isValidOption(item)) { - invalidOptions.push(item); - } - }); - if (invalidOptions.length) { - var optionsString = '"' + invalidOptions.join(", ") + '"'; - var phrase = - invalidOptions.length === 1 - ? "is not a valid option" - : "are not valid options"; - var ending = ". Please change selection."; - var message = optionsString + " " + phrase + ending; - view.showMessage(message, "error", true); + postRender() { + const view = this; + view.trigger("postRender"); + + // Add tool tips for the description + this.$el.find(".item").each((_i, item) => { + view.addTooltip(item); + }); + + // Show an error message if the pre-selected options are not in the + // list of available options (only if user additions are not allowed) + if (!view.allowAdditions) { + if (view.selected && view.selected.length) { + const invalidOptions = []; + view.selected.forEach((item) => { + if (!view.isValidOption(item)) { + invalidOptions.push(item); } + }); + if (invalidOptions.length) { + const optionsString = `"${invalidOptions.join(", ")}"`; + const phrase = + invalidOptions.length === 1 + ? "is not a valid option" + : "are not valid options"; + const ending = ". Please change selection."; + const message = `${optionsString} ${phrase}${ending}`; + view.showMessage(message, "error", true); } } + } - // Set the selected values in the dropdown - this.$selectUI.dropdown("set exactly", view.selected); - this.$selectUI.dropdown("save defaults"); - this.enable(); - this.hideLoading(); - - // Make sub-menus if the option is configured in this view - if (this.submenuStyle === "popout") { - this.convertToPopout(); - } else if (this.submenuStyle === "accordion") { - this.convertToAccordion(); - } + // Set the selected values in the dropdown + this.$selectUI.dropdown("set exactly", view.selected); + this.$selectUI.dropdown("save defaults"); + this.enable(); + this.hideLoading(); + + // Make sub-menus if the option is configured in this view + if (this.submenuStyle === "popout") { + this.convertToPopout(); + } else if (this.submenuStyle === "accordion") { + this.convertToAccordion(); + } - // Convert interactive submenus to lists and hide empty categories - // when the user is searching for a term - if ( - ["popout", "accordion"].includes(view.submenuStyle) || - view.hideEmptyCategoriesOnSearch - ) { - this.$selectUI.find("input").on("keyup blur", function (e) { - inputVal = e.target.value; - - // When the input is NOT empty - if (inputVal !== "") { - // For interactive type submenus where items are sometimes - // hidden, show all the matching items when a user is searching - if (["popout", "accordion"].includes(view.submenuStyle)) { - view.convertToList(); - } - if (view.hideEmptyCategoriesOnSearch) { - view.hideEmptyCategories(); - } + // Convert interactive submenus to lists and hide empty categories + // when the user is searching for a term + if ( + ["popout", "accordion"].includes(view.submenuStyle) || + view.hideEmptyCategoriesOnSearch + ) { + this.$selectUI.find("input").on("keyup blur", (e) => { + const inputVal = e.target.value; + + // When the input is NOT empty + if (inputVal !== "") { + // For interactive type submenus where items are sometimes + // hidden, show all the matching items when a user is searching + if (["popout", "accordion"].includes(view.submenuStyle)) { + view.convertToList(); + } + if (view.hideEmptyCategoriesOnSearch) { + view.hideEmptyCategories(); + } - // When the input is EMPTY - } else { - // Convert back to sub-menus if the option is configured in this view - if (view.submenuStyle === "popout") { - view.convertToPopout(); - } else if (view.submenuStyle === "accordion") { - view.convertToAccordion(); - } - // Show all the category titles again, in cases some where hidden - if (view.hideEmptyCategoriesOnSearch) { - view.showAllCategories(); - } + // When the input is EMPTY + } else { + // Convert back to sub-menus if the option is configured in this view + if (view.submenuStyle === "popout") { + view.convertToPopout(); + } else if (view.submenuStyle === "accordion") { + view.convertToAccordion(); } - }); - } + // Show all the category titles again, in cases some where hidden + if (view.hideEmptyCategoriesOnSearch) { + view.showAllCategories(); + } + } + }); + } - // Trigger an event when the user focuses in searchable inputs - var inputEl = this.$el.find("input.search"); - if (inputEl) { - inputEl.off("focus"); - inputEl.on("focus", function (event) { - view.trigger("inputFocus", event); - }); - } - } catch (e) { - console.log( - "The searchable select post-render function failed, error message: " + - e, - ); + // Trigger an event when the user focuses in searchable inputs + const inputEl = this.$el.find("input.search"); + if (inputEl) { + inputEl.off("focus"); + inputEl.on("focus", (event) => { + view.trigger("inputFocus", event); + }); } }, /** * isValidOption - Checks if a value is one of the values given in view.options - * * @param {string} value The value to check - * @return {boolean} returns true if the value is one of the values given in + * @returns {boolean} returns true if the value is one of the values given in * view.options */ - isValidOption: function (value) { - try { - var view = this; - var options = view.options; - - // If there are no options set on the view, assume the value is invalid - if (!options || options.length === 0) { - return false; - } + isValidOption(value) { + const view = this; + let { options } = view; - // If the list of options doesn't have category headings, put it in the - // same format as options that do have headings. - if (Array.isArray(options)) { - options = { "": options }; - } + // If there are no options set on the view, assume the value is invalid + if (!options || options.length === 0) { + return false; + } - // Reduce the options object to just an Array of value and label strings - var validValues = _(options) - .chain() - .values() - .flatten() - .map(function (item) { - var items = []; - if (item.value !== undefined) { - items.push(item.value); - } - if (item.label !== undefined) { - items.push(item.label); - } - return items; - }) - .flatten() - .value(); - - return validValues.includes(value); - } catch (e) { - console.log( - "Failed to check if an option is valid in a Searchable Select View, error message: " + - e, - ); + // If the list of options doesn't have category headings, put it in the + // same format as options that do have headings. + if (Array.isArray(options)) { + options = { "": options }; } + + // Reduce the options object to just an Array of value and label strings + const validValues = _(options) + .chain() + .values() + .flatten() + .map((item) => { + const items = []; + if (item.value !== undefined) { + items.push(item.value); + } + if (item.label !== undefined) { + items.push(item.label); + } + return items; + }) + .flatten() + .value(); + + return validValues.includes(value); }, /** * addTooltip - Add a tooltip to a given element using the description in the * options object that's set on the view. - * * @param {HTMLElement} element The HTML element a tooltip should be added * @param {string} position how to position the tooltip - top | bottom | left | * right - * @return {jQuery} The element with a tooltip wrapped by jQuery + * @returns {jQuery} The element with a tooltip wrapped by jQuery */ - addTooltip: function (element, position = "bottom") { - try { - if (!element) { - return; - } - - // Find the description in the options object, using the data-value - // attribute set in the template. The data-value attribute is either - // the label, or the value, depending on if a value is provided. - var valueOrLabel = $(element).data("value"); - if (typeof valueOrLabel === "undefined") { - return; - } - if (typeof valueOrLabel === "boolean") { - valueOrLabel = valueOrLabel.toString(); - } - var opt = _.chain(this.options) - .values() - .flatten() - .find(function (option) { - return ( - option.label == valueOrLabel || option.value == valueOrLabel - ); - }) - .value(); - - if (!opt) { - return; - } - if (!opt.description) { - return; - } - - $(element) - .tooltip({ - title: opt.description, - placement: position, - container: "body", - delay: { - show: 900, - hide: 50, - }, - }) - .on("show.bs.popover", function () { - var $el = $(this); - // Allow time for the popup to be added to the DOM - setTimeout(function () { - // Then add a special class to identify - // these popups if they need to be removed. - $el.data("tooltip").$tip.addClass("search-select-tooltip"); - }, 10); - }); + addTooltip(element, position = "bottom") { + if (!element) { + return $(element); + } + // Find the description in the options object, using the data-value + // attribute set in the template. The data-value attribute is either + // the label, or the value, depending on if a value is provided. + let valueOrLabel = $(element).data("value"); + if (typeof valueOrLabel === "undefined") { return $(element); - } catch (e) { - console.log( - "Failed to add tooltip in a searchable select view, error message: " + - e, - ); } + if (typeof valueOrLabel === "boolean") { + valueOrLabel = valueOrLabel.toString(); + } + const opt = _.chain(this.options) + .values() + .flatten() + .find( + (option) => + option.label === valueOrLabel || option.value === valueOrLabel, + ) + .value(); + + if (!opt) { + return $(element); + } + if (!opt.description) { + return $(element); + } + + $(element) + .tooltip({ + title: opt.description, + placement: position, + container: "body", + delay: { + show: 900, + hide: 50, + }, + }) + .on("show.bs.popover", (e) => { + const $el = $(e.target); + // Allow time for the popup to be added to the DOM + setTimeout(() => { + // Add class to identify popups when they need to be removed. + $el.data("tooltip").$tip.addClass("search-select-tooltip"); + }, 10); + }); + + return $(element); }, /** * convertToPopout - Re-arrange the HTML to display category contents * as sub-menus that popout to the left or right of category titles */ - convertToPopout: function () { - try { - if (!this.$selectUI) { - return; - } - if (this.currentSubmenuMode === "popout") { - return; - } - this.currentSubmenuMode = "popout"; - this.$selectUI.addClass("popout-mode"); - var $headers = this.$selectUI.find(".header"); - if (!$headers || $headers.length === 0) { - return; + convertToPopout() { + if (!this.$selectUI) { + return; + } + if (this.currentSubmenuMode === "popout") { + return; + } + this.currentSubmenuMode = "popout"; + this.$selectUI.addClass("popout-mode"); + const $headers = this.$selectUI.find(".header"); + if (!$headers || $headers.length === 0) { + return; + } + $headers.each((_i, header) => { + const $header = $(header); + const $itemGroup = $().add($header.nextUntil(".header")); + const $itemAndHeaderGroup = $header.add($header.nextUntil(".header")); + const $icon = $header.next().find(".icon"); + if ($icon && $icon.length > 0) { + const $headerIcon = $icon.clone().addClass("popout-mode-icon").css({ + opacity: "0.9", + "margin-right": "1rem", + }); + $header.prepend($headerIcon[0]); } - $headers.each(function (i) { - var $itemGroup = $().add($(this).nextUntil(".header")); - var $itemAndHeaderGroup = $(this).add($(this).nextUntil(".header")); - var $icon = $(this).next().find(".icon"); - if ($icon && $icon.length > 0) { - var $headerIcon = $icon.clone().addClass("popout-mode-icon").css({ - opacity: "0.9", - "margin-right": "1rem", - }); - $(this).prepend($headerIcon[0]); - } - $itemAndHeaderGroup.wrapAll("
"); - $itemGroup.wrapAll("