faloker/purify

View on GitHub
web/src/components/dialogs/StepperConfigurator.vue

Summary

Maintainability
Test Coverage
F
40%
<template>
  <div>
    <v-dialog
      v-model="value"
      max-width="60%"
      @input="$emit('input', $event.target.value)"
      @click:outside="resetDialog"
      @keydown.esc="resetDialog"
    >
      <v-stepper
        v-model="stepperModel"
        non-linear
        vertical
      >
        <v-stepper-step
          :complete="stepperModel > 1 || report.type === 'oneshot'"
          step="1"
        >
          Select the array with the findings. You can view an example finding object in an array.
        </v-stepper-step>
        <v-stepper-content step="1">
          <v-btn
            class="mx-2"
            outlined
            @click.stop="reportDialog = true"
          >
            View report
          </v-btn>
          <v-btn
            class="mx-2"
            :disabled="!pathToIssues"
            outlined
            color="primary"
            @click.stop="stepperModel = 2"
          >
            Next
          </v-btn>
          <v-card-text>
            You have chosen the following:
            <b class="ml-3">{{ pathToIssues }}</b>
          </v-card-text>
        </v-stepper-content>
        <v-stepper-step
          :complete="stepperModel > 2"
          step="2"
        >
          Which fields will be in issue title and subtitle?
        </v-stepper-step>
        <v-stepper-content step="2">
          <vue-json-pretty
            v-model="titleFields"
            :deep="2"
            :path="'issue'"
            :selectable-type="'multiple'"
            :path-selectable="(path, data) => path !== 'issue'"
            :data="exampleIssue"
            :show-select-controller="true"
            :highlight-selected-node="false"
          />
          <v-btn
            class="mx-2 mt-3"
            outlined
            :disabled="!titleFields.length"
            color="primary"
            @click.stop="stepperModel = 3"
          >
            Next
          </v-btn>
          <v-btn
            class="mx-2 mt-3"
            outlined
            @click.stop="stepperModel = 1"
          >
            Go back
          </v-btn>
        </v-stepper-content>
        <v-stepper-step
          :complete="stepperModel > 3"
          step="3"
        >
          Which field represent the severity of the finding? (optional)
        </v-stepper-step>
        <v-stepper-content step="3">
          <vue-json-pretty
            v-model="riskField"
            :deep="2"
            :path="'issue'"
            :selectable-type="'multiple'"
            :path-selectable="(path, data) => path !== 'issue'"
            :data="exampleIssue"
            :show-select-controller="true"
            :highlight-selected-node="false"
          />
          <v-btn
            class="mx-2 mt-3"
            outlined
            color="primary"
            :disabled="riskField.length > 1"
            @click.stop="stepperModel = 4"
          >
            Next
          </v-btn>
          <v-btn
            class="mx-2 mt-3"
            outlined
            @click.stop="stepperModel = 2"
          >
            Go back
          </v-btn>
        </v-stepper-content>
        <v-stepper-step
          :complete="stepperModel > 4"
          step="4"
        >
          Add patterns to display title and subtitle
        </v-stepper-step>
        <v-stepper-content step="4">
          <v-combobox
            v-model="titlePattern"
            label="Pattern for a title"
            hint="For example, [issue.severity] -- issue.title"
            persistent-hint
            :items="titleFields.map(i => i.replace('issue.', ''))"
          />
          <v-combobox
            v-model="subtitlePattern"
            class="pt-4"
            label="Pattern for a subtitle"
            hint="For example, issue.CVSS | issue.short_description"
            persistent-hint
            :items="titleFields.map(i => i.replace('issue.', ''))"
          />
          <v-btn
            :disabled="!titleFields.length"
            color="primary"
            class="mx-2 mt-3"
            outlined
            @click.stop="stepperModel = 5"
          >
            Next
          </v-btn>
          <v-btn
            class="mx-2 mt-3"
            outlined
            @click.stop="stepperModel = 3"
          >
            Go back
          </v-btn>
        </v-stepper-content>
        <v-stepper-step :complete="stepperModel > 5" step="5">
          Which fields will be in issue body?
        </v-stepper-step>
        <v-stepper-content step="5">
          <vue-json-pretty
            v-model="bodyFields"
            :deep="2"
            :path="'issue'"
            :selectable-type="'multiple'"
            :path-selectable="(path, data) => path !== 'issue'"
            :data="exampleIssue"
            :show-select-controller="true"
            :highlight-selected-node="false"
          />
          <v-btn
            :disabled="!bodyFields.length"
            color="primary"
            class="mx-2 mt-3"
            outlined
            @click="prepareBodyFields"
          >
            Next
          </v-btn>
          <v-btn
            class="mx-2 mt-3"
            outlined
            @click="stepperModel = 4"
          >
            Go back
          </v-btn>
        </v-stepper-content>
        <v-stepper-step :complete="stepperModel > 6" step="6">
          Select types of fields in issue body
        </v-stepper-step>
        <v-stepper-content step="6">
          <template v-for="item in bodyFields">
            <v-row
              :key="item"
              dense
              align="start"
              justify="start"
            >
              <v-col cols="2">
                <span class="subtitle-1">{{ item.replace('issue.', '') }}</span>
              </v-col>
              <v-col cols="1">
                <v-select
                  v-model="bodyFieldsTypes[item]"
                  :items="fieldTypes"
                  label="Type"
                  outlined
                  dense
                />
              </v-col>
              <v-col cols="2">
                <v-text-field
                  v-model="bodyFieldsNames[item]"
                  label="Display Name"
                  outlined
                  dense
                />
              </v-col>
            </v-row>
          </template>
          <v-btn
            :disabled="Object.keys(bodyFieldsTypes).length !== bodyFields.length"
            color="primary"
            class="mr-2 mt-3"
            outlined
            @click.stop="stepperModel = 7"
          >
            Next
          </v-btn>
          <v-btn
            class="mr-2 mt-3"
            outlined
            @click.stop="stepperModel = 5"
          >
            Go back
          </v-btn>
        </v-stepper-content>
        <v-stepper-step
          :complete="stepperModel > 7"
          step="7"
        >
          Select field(s) to detect duplicates and merge candidates at the template level
        </v-stepper-step>
        <v-stepper-content step="7">
          <vue-json-pretty
            v-model="internalComparisonFields"
            :deep="2"
            :path="'issue'"
            :selectable-type="'multiple'"
            :path-selectable="(path, data) => path !== 'issue'"
            :data="exampleIssue"
            :show-select-controller="true"
            :highlight-selected-node="false"
          />
          <v-btn
            :disabled="!internalComparisonFields.length"
            color="primary"
            class="mr-2 mt-3"
            outlined
            @click.stop="stepperModel = 8"
          >
            Next
          </v-btn>
          <v-btn
            class="mr-2 mt-3"
            outlined
            @click.stop="stepperModel = 6"
          >
            Go back
          </v-btn>
        </v-stepper-content>
        <v-stepper-step
          :complete="stepperModel > 8"
          step="8"
        >
          Select field(s) to merge if issues look the same at the template level (optional)
        </v-stepper-step>
        <v-stepper-content step="8">
          <vue-json-pretty
            v-model="mergeFields"
            :deep="2"
            :path="'issue'"
            :selectable-type="'multiple'"
            :path-selectable="(path, data) => path !== 'issue'"
            :data="exampleIssue"
            :show-select-controller="true"
            :highlight-selected-node="false"
          />
          <v-btn
            color="primary"
            class="mr-2 mt-3"
            outlined
            @click.stop="stepperModel = 9"
          >
            Next
          </v-btn>
          <v-btn
            class="mr-2 mt-3"
            outlined
            @click.stop="stepperModel = 7"
          >
            Go back
          </v-btn>
        </v-stepper-content>
        <v-stepper-step
          :complete="stepperModel > 9"
          step="9"
        >
          Select field(s) to detect duplicates at the unit level
        </v-stepper-step>
        <v-stepper-content step="9">
          <vue-json-pretty
            v-model="externalComparisonFields"
            :deep="2"
            :path="'issue'"
            :selectable-type="'multiple'"
            :path-selectable="(path, data) => path !== 'issue'"
            :data="exampleIssue"
            :show-select-controller="true"
            :highlight-selected-node="false"
          />
          <v-btn
            :disabled="!externalComparisonFields.length"
            color="primary"
            class="mr-2 mt-3"
            outlined
            @click.stop="stepperModel = 10"
          >
            Next
          </v-btn>
          <v-btn
            class="mr-2 mt-3"
            outlined
            @click.stop="stepperModel = 8"
          >
            Go back
          </v-btn>
        </v-stepper-content>
        <v-stepper-step step="10">
          Give a name for this template
        </v-stepper-step>
        <v-stepper-content step="10">
          <v-text-field
            v-model="displayName"
            class="tname mt-1"
            :rules="[rules.min]"
            label="Template Name"
            prepend-icon="short_text"
            clearable
            dense
            outlined
            required
          />
          <v-text-field
            id="name"
            v-model="name"
            class="tname"
            label="Short name"
            hint="The short name is used as the unique ID within URLs."
            dense
            outlined
            persistent-hint
            prepend-icon="mdi-identifier"
            append-icon="mdi-auto-fix"
            @click:append="slugifyDisplayName"
          />
          <v-combobox
            v-model="tags"
            class="tname"
            label="Add some tags"
            prepend-icon="mdi-tag-multiple"
            chips
            small-chips
            multiple
            dense
            outlined
          />
          <v-btn
            color="primary"
            class="mr-2 mt-3"
            outlined
            :loading="loading"
            :disabled="loading"
            @click="createTemplate()"
          >
            Save
          </v-btn>
          <v-btn
            class="mr-2 mt-3"
            outlined
            @click="stepperModel = 9"
          >
            Go back
          </v-btn>
        </v-stepper-content>
      </v-stepper>
    </v-dialog>
    <v-dialog
      v-model="reportDialog"
      persistent
      scrollable
      max-width="60%"
    >
      <v-card class="fill-height">
        <div class="my-5 ml-5">
          <vue-json-pretty
            v-model="pathToIssues"
            :deep="1"
            path="report"
            show-length
            :selectable-type="'single'"
            :data="reportContent"
            :show-select-controller="true"
            :highlight-selected-node="false"
          />
        </div>
        <v-card-actions>
          <v-spacer />
          <v-btn
            color="green"
            outlined
            block
            @click.stop="reportDialog = false"
          >
            Confirm
          </v-btn>
          <v-spacer />
        </v-card-actions>
      </v-card>
    </v-dialog>
  </div>
</template>
<script lang="ts">
/* eslint-disable @typescript-eslint/no-use-before-define */
import {
  defineComponent,
  ref,
  computed,
  PropType,
  watch,
  Ref,
} from '@vue/composition-api';
// @ts-ignore
import VueJsonPretty from 'vue-json-pretty';
import {
  TEMPLATE_CREATE,
  FETCH_REPORTS,
  APPLY_TEMPLATE,
} from '@/store/actions';
import { Report } from '@/store/types';
import store from '@/store';
import slug from 'slug';
import { get } from 'lodash';
import { parseKey } from '@/utils/helpers';

interface BodyField {
  key: string;
  type: string;
  alias?: string;
}

export default defineComponent({
  name: 'StepperConfigurator',

  components: {
    VueJsonPretty,
  },

  props: {
    value: {
      type: Boolean,
      default: false,
    },
    report: {
      type: Object as PropType<Report>,
      required: true,
    },
  },

  setup(props, { emit }) {
    const stepperModel = ref(1);
    const reportDialog = ref(false);
    const loading = ref(false);
    const rules = ref({
      min: (v: string) => v.length >= 3 || 'Min 3 symbols',
    });

    const name = ref('');
    const displayName = ref('');
    const mergeFields: Ref<string[]> = ref([]);
    const internalComparisonFields: Ref<string[]> = ref([]);
    const externalComparisonFields: Ref<string[]> = ref([]);
    const titlePattern = ref('');
    const subtitlePattern = ref('');
    const tags: Ref<string[]> = ref([]);
    const pathToIssues = ref('');
    const titleFields: Ref<string[]> = ref([]);
    const bodyFields: Ref<BodyField[]> = ref([]);
    const riskField: Ref<string[]> = ref([]);
    const bodyFieldsTypes = ref({});
    const bodyFieldsNames = ref({});
    const fieldTypes = ref(['text', 'html', 'base64']);

    const reportContent = computed(() => store.state.reports.content);
    const exampleIssue = computed(() => {
      return props.report.type === 'file'
        ? get(reportContent.value, pathToIssues.value.replace('report.', ''))
        : reportContent.value;
    });

    function resetDialog() {
      emit('input', false);
      stepperModel.value = props.report.type === 'file' ? 1 : 2;
      name.value = '';
      titlePattern.value = '';
      subtitlePattern.value = '';
      pathToIssues.value = '';
      bodyFieldsTypes.value = {};
      tags.value = [];
      internalComparisonFields.value = [];
      externalComparisonFields.value = [];
      mergeFields.value = [];
      riskField.value = [];
      titleFields.value = [];
      bodyFields.value = [];
    }

    function slugifyDisplayName() {
      name.value = slug(displayName.value);
    }

    function createTemplate() {
      loading.value = true;
      bodyFields.value = [];

      for (const key of Object.keys(bodyFieldsTypes.value)) {
        bodyFields.value.push({
          key: key.replace('issue.', ''),
          // @ts-ignore
          type: bodyFieldsTypes.value[key],
          // @ts-ignore
          alias: bodyFieldsNames.value[key],
        });
      }

      store
        .dispatch(TEMPLATE_CREATE, {
          pathToIssues: pathToIssues.value
            .replace('report.root.', '')
            .replace('[0]', '')
            .replace('report.root', ''),
          name: name.value,
          displayName: displayName.value,
          titlePattern: titlePattern.value,
          subtitlePattern: subtitlePattern.value,
          tags: tags.value,
          bodyFields: bodyFields.value,
          riskField: riskField.value.map((i) => i.replace('issue.', ''))[0],
          mergeFields: mergeFields.value.map((i) => i.replace('issue.', '')),
          titleFields: titleFields.value.map((i) => i.replace('issue.', '')),
          internalComparisonFields: internalComparisonFields.value.map((i) =>
            i.replace('issue.', '')
          ),
          externalComparisonFields: externalComparisonFields.value.map((i) =>
            i.replace('issue.', '')
          ),
        })
        .then(() => {
          store
            .dispatch(APPLY_TEMPLATE, {
              reportId: props.report._id,
              templateName: name.value,
            })
            .then(() => {
              loading.value = false;
            });
        })
        .catch(() => {
          loading.value = false;
        });

      store
        .dispatch(FETCH_REPORTS)
        .then(() => {
          resetDialog();
        })
        .catch(() => {});
    }
    watch(
      () => props.value,
      () => {
        stepperModel.value = props.report.type === 'file' ? 1 : 2;
      }
    );

    function prepareBodyFields() {
      // @ts-ignore
      bodyFields.value.forEach((field: string) => {
        // @ts-ignore
        bodyFieldsTypes.value[field] = 'text';
        // @ts-ignore
        bodyFieldsNames.value[field] = parseKey(field.replace('issue.', ''));
      });
      stepperModel.value = 6;
    }

    return {
      fieldTypes,
      prepareBodyFields,
      rules,
      stepperModel,
      reportDialog,
      name,
      displayName,
      reportContent,
      resetDialog,
      tags,
      exampleIssue,
      loading,
      slugifyDisplayName,
      riskField,
      bodyFields,
      mergeFields,
      titleFields,
      titlePattern,
      pathToIssues,
      createTemplate,
      subtitlePattern,
      bodyFieldsTypes,
      bodyFieldsNames,
      externalComparisonFields,
      internalComparisonFields,
      parseKey,
    };
  },
});
</script>

<style scoped>
.tname {
  width: 300px;
}
</style>