Add SSL Certificates page

Adds ability to view, add, replace, and delete SSL
certificates in GUI.

Signed-off-by: Yoshie Muranaka <yoshiemuranaka@gmail.com>
Change-Id: I5cf9fa7bbd588dfb22f2431eed0b5976ff860703
diff --git a/src/views/AccessControl/SslCertificates/ModalUploadCertificate.vue b/src/views/AccessControl/SslCertificates/ModalUploadCertificate.vue
new file mode 100644
index 0000000..653a232
--- /dev/null
+++ b/src/views/AccessControl/SslCertificates/ModalUploadCertificate.vue
@@ -0,0 +1,164 @@
+<template>
+  <b-modal id="upload-certificate" ref="modal" @ok="onOk" @hidden="resetForm">
+    <template v-slot:modal-title>
+      <template v-if="certificate">
+        {{ $t('pageSslCertificates.replaceCertificate') }}
+      </template>
+      <template v-else>
+        {{ $t('pageSslCertificates.addNewCertificate') }}
+      </template>
+    </template>
+    <b-form>
+      <!-- Replace Certificate type -->
+      <template v-if="certificate !== null">
+        <dl class="mb-4">
+          <dt>{{ $t('pageSslCertificates.modal.certificateType') }}</dt>
+          <dd>{{ certificate.certificate }}</dd>
+        </dl>
+      </template>
+
+      <!-- Add new Certificate type -->
+      <template v-else>
+        <b-form-group
+          :label="$t('pageSslCertificates.modal.certificateType')"
+          label-for="certificate-type"
+        >
+          <b-form-select
+            id="certificate-type"
+            v-model="form.certificateType"
+            :options="certificateOptions"
+            :state="getValidationState($v.form.certificateType)"
+            @input="$v.form.certificateType.$touch()"
+          >
+          </b-form-select>
+          <b-form-invalid-feedback role="alert">
+            <template v-if="!$v.form.certificateType.required">
+              {{ $t('global.form.fieldRequired') }}
+            </template>
+          </b-form-invalid-feedback>
+        </b-form-group>
+      </template>
+
+      <b-form-group
+        :label="$t('pageSslCertificates.modal.certificateFile')"
+        label-for="certificate-file"
+      >
+        <b-form-file
+          id="certificate-file"
+          v-model="form.file"
+          accept=".pem"
+          plain
+          :state="getValidationState($v.form.file)"
+        />
+        <b-form-invalid-feedback role="alert">
+          <template v-if="!$v.form.file.required">
+            {{ $t('global.form.required') }}
+          </template>
+        </b-form-invalid-feedback>
+      </b-form-group>
+    </b-form>
+    <template v-slot:modal-ok>
+      <template v-if="certificate">
+        {{ $t('global.action.replace') }}
+      </template>
+      <template v-else>
+        {{ $t('global.action.add') }}
+      </template>
+    </template>
+  </b-modal>
+</template>
+
+<script>
+import { required, requiredIf } from 'vuelidate/lib/validators';
+import VuelidateMixin from '../../../components/Mixins/VuelidateMixin.js';
+
+export default {
+  mixins: [VuelidateMixin],
+  props: {
+    certificate: {
+      type: Object,
+      default: null,
+      validator: prop => {
+        if (prop === null) return true;
+        return (
+          prop.hasOwnProperty('type') && prop.hasOwnProperty('certificate')
+        );
+      }
+    }
+  },
+  data() {
+    return {
+      form: {
+        certificateType: null,
+        file: null
+      }
+    };
+  },
+  computed: {
+    certificateTypes() {
+      return this.$store.getters['sslCertificates/availableUploadTypes'];
+    },
+    certificateOptions() {
+      return this.certificateTypes.map(({ type, label }) => {
+        return {
+          text: label,
+          value: type
+        };
+      });
+    }
+  },
+  watch: {
+    certificateOptions: function(options) {
+      if (options.length) {
+        this.form.certificateType = options[0].value;
+      }
+    }
+  },
+  validations() {
+    return {
+      form: {
+        certificateType: {
+          required: requiredIf(function() {
+            return !this.certificate;
+          })
+        },
+        file: {
+          required
+        }
+      }
+    };
+  },
+  methods: {
+    handleSubmit() {
+      this.$v.$touch();
+      if (this.$v.$invalid) return;
+      this.$emit('ok', {
+        addNew: !this.certificate,
+        file: this.form.file,
+        location: this.certificate ? this.certificate.location : null,
+        type: this.certificate
+          ? this.certificate.type
+          : this.form.certificateType
+      });
+      this.closeModal();
+    },
+    closeModal() {
+      this.$nextTick(() => {
+        this.$refs.modal.hide();
+      });
+    },
+    resetForm() {
+      this.form.certificateType = this.certificateOptions.length
+        ? this.certificateOptions[0].value
+        : null;
+      this.form.file = null;
+      this.$v.$reset();
+    },
+    onOk(bvModalEvt) {
+      // prevent modal close
+      bvModalEvt.preventDefault();
+      this.handleSubmit();
+    }
+  }
+};
+</script>
diff --git a/src/views/AccessControl/SslCertificates/SslCertificates.vue b/src/views/AccessControl/SslCertificates/SslCertificates.vue
new file mode 100644
index 0000000..ae28271
--- /dev/null
+++ b/src/views/AccessControl/SslCertificates/SslCertificates.vue
@@ -0,0 +1,209 @@
+<template>
+  <b-container fluid>
+    <page-title />
+    <b-row>
+      <b-col xl="9" class="text-right">
+        <b-button
+          variant="primary"
+          :disabled="certificatesForUpload.length === 0"
+          @click="initModalUploadCertificate(null)"
+        >
+          <icon-add />
+          {{ $t('pageSslCertificates.addNewCertificate') }}
+        </b-button>
+      </b-col>
+    </b-row>
+    <b-row>
+      <b-col xl="9">
+        <b-table :fields="fields" :items="tableItems">
+          <template v-slot:cell(validFrom)="{ value }">
+            {{ value | formatDate }}
+          </template>
+
+          <template v-slot:cell(validUntil)="{ value }">
+            {{ value | formatDate }}
+          </template>
+
+          <template v-slot:cell(actions)="{ value, item }">
+            <table-row-action
+              v-for="(action, index) in value"
+              :key="index"
+              :value="action.value"
+              :title="action.title"
+              :enabled="action.enabled"
+              @click:tableAction="onTableRowAction($event, item)"
+            >
+              <template v-slot:icon>
+                <icon-replace v-if="action.value === 'replace'" />
+                <icon-trashcan v-if="action.value === 'delete'" />
+              </template>
+            </table-row-action>
+          </template>
+        </b-table>
+      </b-col>
+    </b-row>
+
+    <!-- Modals -->
+    <modal-upload-certificate :certificate="modalCertificate" @ok="onModalOk" />
+  </b-container>
+</template>
+
+<script>
+import IconAdd from '@carbon/icons-vue/es/add--alt/20';
+import IconReplace from '@carbon/icons-vue/es/renew/20';
+import IconTrashcan from '@carbon/icons-vue/es/trash-can/20';
+
+import ModalUploadCertificate from './ModalUploadCertificate';
+import PageTitle from '../../../components/Global/PageTitle';
+import TableRowAction from '../../../components/Global/TableRowAction';
+
+import BVToastMixin from '../../../components/Mixins/BVToastMixin';
+
+export default {
+  name: 'SslCertificates',
+  components: {
+    IconAdd,
+    IconReplace,
+    IconTrashcan,
+    ModalUploadCertificate,
+    PageTitle,
+    TableRowAction
+  },
+  mixins: [BVToastMixin],
+  data() {
+    return {
+      modalCertificate: null,
+      fields: [
+        {
+          key: 'certificate',
+          label: this.$t('pageSslCertificates.table.certificate')
+        },
+        {
+          key: 'issuedBy',
+          label: this.$t('pageSslCertificates.table.issuedBy')
+        },
+        {
+          key: 'issuedTo',
+          label: this.$t('pageSslCertificates.table.issuedTo')
+        },
+        {
+          key: 'validFrom',
+          label: this.$t('pageSslCertificates.table.validFrom')
+        },
+        {
+          key: 'validUntil',
+          label: this.$t('pageSslCertificates.table.validUntil')
+        },
+        {
+          key: 'actions',
+          label: '',
+          tdClass: 'text-right'
+        }
+      ]
+    };
+  },
+  computed: {
+    certificates() {
+      return this.$store.getters['sslCertificates/allCertificates'];
+    },
+    tableItems() {
+      return this.certificates.map(certificate => {
+        return {
+          ...certificate,
+          actions: [
+            {
+              value: 'replace',
+              title: this.$t('pageSslCertificates.replaceCertificate')
+            },
+            {
+              value: 'delete',
+              title: this.$t('pageSslCertificates.deleteCertificate'),
+              enabled:
+                certificate.type === 'TrustStore Certificate' ? true : false
+            }
+          ]
+        };
+      });
+    },
+    certificatesForUpload() {
+      return this.$store.getters['sslCertificates/availableUploadTypes'];
+    }
+  },
+  created() {
+    this.$store.dispatch('sslCertificates/getCertificates');
+  },
+  methods: {
+    onTableRowAction(event, rowItem) {
+      switch (event) {
+        case 'replace':
+          this.initModalUploadCertificate(rowItem);
+          break;
+        case 'delete':
+          this.initModalDeleteCertificate(rowItem);
+          break;
+        default:
+          break;
+      }
+    },
+    initModalUploadCertificate(certificate = null) {
+      this.modalCertificate = certificate;
+      this.$bvModal.show('upload-certificate');
+    },
+    initModalDeleteCertificate(certificate) {
+      this.$bvModal
+        .msgBoxConfirm(
+          this.$t('pageSslCertificates.modal.deleteConfirmMessage', {
+            issuedBy: certificate.issuedBy,
+            certificate: certificate.certificate
+          }),
+          {
+            title: this.$t('pageSslCertificates.deleteCertificate'),
+            okTitle: this.$t('global.action.delete')
+          }
+        )
+        .then(deleteConfirmed => {
+          if (deleteConfirmed) this.deleteCertificate(certificate);
+        });
+    },
+    onModalOk({ addNew, file, type, location }) {
+      if (addNew) {
+        // Upload a new certificate
+        this.addNewCertificate(file, type);
+      } else {
+        // Replace an existing certificate
+        this.replaceCertificate(file, type, location);
+      }
+    },
+    addNewCertificate(file, type) {
+      this.$store
+        .dispatch('sslCertificates/addNewCertificate', { file, type })
+        .then(success => this.successToast(success))
+        .catch(({ message }) => this.errorToast(message));
+    },
+    replaceCertificate(file, type, location) {
+      const reader = new FileReader();
+      reader.readAsBinaryString(file);
+      reader.onloadend = event => {
+        const certificateString = event.target.result;
+        this.$store
+          .dispatch('sslCertificates/replaceCertificate', {
+            certificateString,
+            type,
+            location
+          })
+          .then(success => this.successToast(success))
+          .catch(({ message }) => this.errorToast(message));
+      };
+    },
+    deleteCertificate({ type, location }) {
+      this.$store
+        .dispatch('sslCertificates/deleteCertificate', {
+          type,
+          location
+        })
+        .then(success => this.successToast(success))
+        .catch(({ message }) => this.errorToast(message));
+    }
+  }
+};
+</script>
diff --git a/src/views/AccessControl/SslCertificates/index.js b/src/views/AccessControl/SslCertificates/index.js
new file mode 100644
index 0000000..03daa56
--- /dev/null
+++ b/src/views/AccessControl/SslCertificates/index.js
@@ -0,0 +1,2 @@
+import SslCertificates from './SslCertificates.vue';
+export default SslCertificates;