mirror of
https://github.com/SecurityBrewery/catalyst.git
synced 2025-12-08 16:22:46 +01:00
Release catalyst
This commit is contained in:
195
ui/src/components/AdvancedJSONSchemaEditor.vue
Normal file
195
ui/src/components/AdvancedJSONSchemaEditor.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="d-flex" >
|
||||
<v-spacer></v-spacer>
|
||||
<v-switch
|
||||
v-model="advanced"
|
||||
label="Advanced"
|
||||
class="float-right mt-0"
|
||||
></v-switch>
|
||||
</div>
|
||||
<div class="d-flex" >
|
||||
<v-spacer></v-spacer>
|
||||
<span v-if="advanced" class="float-right">
|
||||
See
|
||||
<a target="_blank" href="https://koumoul-dev.github.io/vuetify-jsonschema-form/latest/">
|
||||
vuetify-jsonschema documentation
|
||||
</a>
|
||||
for styling.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<v-row class="flex-grow-0 flex-shrink-0">
|
||||
<v-col :cols="hidepreview ? 12 : 7">
|
||||
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">
|
||||
Schema
|
||||
</v-subheader>
|
||||
<div v-if="!advanced">
|
||||
<json-schema-editor :disabled="readonly" :value="{ root: internalSchema }" lang="en_US" style="border: 1px solid #393a3f" class="mb-3 rounded" />
|
||||
</div>
|
||||
<div v-else class="flex-grow-1 flex-shrink-1 overflow-scroll">
|
||||
<Editor v-model="schemaString" lang="json" :readonly="readonly"></Editor>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col v-if="!hidepreview" cols="5">
|
||||
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">
|
||||
Form output preview
|
||||
</v-subheader>
|
||||
<v-form v-model="valid">
|
||||
<v-jsf
|
||||
v-model="details"
|
||||
:schema="advanced ? parsedSchemaString : internalSchema"
|
||||
:options="{ readonly: true, formats: { time: timeformat, date: dateformat, 'date-time': datetimeformat } }"
|
||||
/>
|
||||
</v-form>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="!readonly" class="px-3 my-6 flex-grow-0 flex-shrink-0">
|
||||
<v-btn v-if="this.$route.params.id === 'new'" color="success" @click="save" outlined>
|
||||
<v-icon>mdi-plus-thick</v-icon>
|
||||
Create
|
||||
</v-btn>
|
||||
<v-btn v-else color="success" @click="save" outlined>
|
||||
<v-icon>mdi-content-save</v-icon>
|
||||
Save
|
||||
</v-btn>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Editor from "./Editor.vue";
|
||||
import Vue from "vue";
|
||||
import {DateTime} from "luxon";
|
||||
|
||||
interface State {
|
||||
advanced: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
internalSchema: any;
|
||||
schemaString: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
details: any;
|
||||
valid: boolean;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "AdvancedJSONSchemaEditor",
|
||||
components: { Editor },
|
||||
props: {
|
||||
schema: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
hidepreview: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
data: (): State => ({
|
||||
details: {},
|
||||
advanced: false,
|
||||
internalSchema: {},
|
||||
schemaString: "{}",
|
||||
valid: true,
|
||||
}),
|
||||
watch: {
|
||||
schema: function () {
|
||||
this.internalSchema = this.schema;
|
||||
this.schemaString = JSON.stringify(this.internalSchema, null, 2);
|
||||
},
|
||||
advanced: function (advanced) {
|
||||
if (advanced) {
|
||||
this.schemaString = JSON.stringify(this.internalSchema, null, 2);
|
||||
} else {
|
||||
this.internalSchema = JSON.parse(this.schemaString);
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
parsedSchemaString: function() {
|
||||
try {
|
||||
return JSON.parse(this.schemaString);
|
||||
}
|
||||
catch (e) {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
save: function () {
|
||||
let schema = this.schemaString;
|
||||
if (!this.advanced) {
|
||||
schema = JSON.stringify(this.internalSchema);
|
||||
}
|
||||
this.$emit("save", schema);
|
||||
},
|
||||
timeformat: function(s: string, locale: string) {
|
||||
let format = this.$store.state.settings.timeformat;
|
||||
if (!format) {
|
||||
return DateTime.fromISO(s).toLocaleString(DateTime.DATETIME_SHORT);
|
||||
}
|
||||
return DateTime.fromISO(s).toFormat(format);
|
||||
},
|
||||
dateformat: function(s: string, locale: string) {
|
||||
let format = this.$store.state.settings.timeformat;
|
||||
if (!format) {
|
||||
return DateTime.fromISO(s).toLocaleString(DateTime.DATETIME_SHORT);
|
||||
}
|
||||
return DateTime.fromISO(s).toFormat(format);
|
||||
},
|
||||
datetimeformat: function(s: string, locale: string) {
|
||||
let format = this.$store.state.settings.timeformat;
|
||||
if (!format) {
|
||||
return DateTime.fromISO(s).toLocaleString(DateTime.DATETIME_SHORT);
|
||||
}
|
||||
return DateTime.fromISO(s).toFormat(format);
|
||||
},
|
||||
hasRole: function (s: string): boolean {
|
||||
if (this.$store.state.user.roles) {
|
||||
return this.lodash.includes(this.$store.state.user.roles, s);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.internalSchema = this.schema;
|
||||
this.schemaString = JSON.stringify(this.internalSchema, null, 2);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.theme--dark .ant-btn,
|
||||
.theme--dark .ant-select-selection,
|
||||
.theme--dark .ant-input,
|
||||
.theme--dark .ant-input-number,
|
||||
.theme--dark .ant-modal-header,
|
||||
.theme--dark .ant-modal-title,
|
||||
.theme--dark .ant-form-item,
|
||||
.theme--dark .ant-modal-close-x,
|
||||
.theme--dark .ant-select-dropdown,
|
||||
.theme--dark .ant-checkbox-inner {
|
||||
color: white !important;
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.theme--dark .ant-select-selection,
|
||||
.theme--dark .ant-input,
|
||||
.theme--dark .ant-input-number,
|
||||
.theme--dark .ant-modal-header,
|
||||
.theme--dark .ant-modal-footer,
|
||||
.theme--dark .ant-checkbox-inner {
|
||||
border-color: #424242 !important;
|
||||
}
|
||||
|
||||
.theme--dark .ant-modal-content,
|
||||
.theme--dark .ant-select-dropdown {
|
||||
color: white !important;
|
||||
background: #303030 !important;
|
||||
}
|
||||
</style>
|
||||
64
ui/src/components/AppLink.vue
Normal file
64
ui/src/components/AppLink.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-list nav dense>
|
||||
<v-list-item v-for="link in userLinks" :key="link.to" link :to="{ name: link.to }">
|
||||
<v-list-item-icon>
|
||||
<v-badge
|
||||
v-if="'count' in link && link.count"
|
||||
:content="link.count"
|
||||
color="red"
|
||||
left
|
||||
offset-x="35"
|
||||
offset-y="8"
|
||||
bottom>
|
||||
<v-icon>{{ link.icon }}</v-icon>
|
||||
</v-badge>
|
||||
<v-icon v-else>{{ link.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title>{{ link.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<v-divider v-if="userLinks.length > 0"></v-divider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
export default Vue.extend({
|
||||
name: "AppLink",
|
||||
props: ["links"],
|
||||
computed: {
|
||||
userLinks: function (): Array<any> {
|
||||
return this.lodash.filter(this.links, link => {
|
||||
return this.hasRole(link) && this.hasTier(link)
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hasRole: function (link: any) {
|
||||
if (!("role" in link)) {
|
||||
return true;
|
||||
}
|
||||
let has = false;
|
||||
if (this.$store.state.user.roles) {
|
||||
this.lodash.forEach(this.$store.state.user.roles, (userRole) => {
|
||||
if (link.role === userRole || this.lodash.startsWith(link.role, userRole + ":")) {
|
||||
has = true;
|
||||
}
|
||||
})
|
||||
}
|
||||
return has;
|
||||
},
|
||||
hasTier: function (link: any): boolean {
|
||||
if ("tier" in link) {
|
||||
if (this.$store.state.settings.tier) {
|
||||
return this.$store.state.settings.tier == link.tier;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
115
ui/src/components/Editor.vue
Normal file
115
ui/src/components/Editor.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<prism-editor
|
||||
v-if="showEditor"
|
||||
class="my-editor"
|
||||
v-model="code"
|
||||
:highlight="highlighter"
|
||||
line-numbers
|
||||
:readonly="readonly">
|
||||
</prism-editor>
|
||||
|
||||
<!--MonacoEditor
|
||||
v-if="showEditor"
|
||||
ref="editor"
|
||||
class="editor"
|
||||
style="height: 100%"
|
||||
v-model="code"
|
||||
:language="this.lang"
|
||||
:options="{ scrollBeyondLastLine: false }"
|
||||
:theme="$vuetify.theme.dark ? 'vs-dark' : 'vs'"
|
||||
/-->
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
// import MonacoEditor from "vue-monaco";
|
||||
|
||||
import { PrismEditor } from 'vue-prism-editor';
|
||||
import 'vue-prism-editor/dist/prismeditor.min.css'; // import the styles somewhere
|
||||
|
||||
import { highlight, languages } from 'prismjs/components/prism-core';
|
||||
// import 'prismjs/components/prism-javascript';
|
||||
import 'prismjs/components/prism-json';
|
||||
import 'prismjs/components/prism-python';
|
||||
import 'prismjs/components/prism-yaml';
|
||||
import 'prismjs/components/prism-markup';
|
||||
import 'prismjs/components/prism-log';
|
||||
import 'prismjs/themes/prism-tomorrow.css'; // import syntax highlighting styles
|
||||
|
||||
interface State {
|
||||
showEditor: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
resize?: any;
|
||||
code: string,
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "Editor",
|
||||
props: ["value", "lang", "readonly"],
|
||||
components: {PrismEditor},
|
||||
data: (): State => ({
|
||||
showEditor: true,
|
||||
resize: undefined,
|
||||
code: "",
|
||||
}),
|
||||
watch: {
|
||||
code: function () {
|
||||
this.$emit('input', this.code);
|
||||
},
|
||||
value: function () {
|
||||
this.code = this.value;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// resizeEditor() {
|
||||
// this.showEditor = false;
|
||||
// this.$nextTick(() => {
|
||||
// this.showEditor = true;
|
||||
// });
|
||||
// },
|
||||
highlighter(code: string) {
|
||||
switch (this.lang) {
|
||||
case "python":
|
||||
return highlight(code, languages.python);
|
||||
case "log":
|
||||
return highlight(code, languages.log);
|
||||
case "yaml":
|
||||
return highlight(code, languages.yaml);
|
||||
case "json":
|
||||
return highlight(code, languages.json);
|
||||
case "html":
|
||||
return highlight(code, languages.html);
|
||||
}
|
||||
return highlight(code, languages.json);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.code = this.value;
|
||||
// this.resize = this.lodash.debounce(this.resizeEditor, 200);
|
||||
// window.addTicketListener("resize", this.resize);
|
||||
},
|
||||
destroyed() {
|
||||
// window.removeticketListener("resize", this.resize);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* required class */
|
||||
.my-editor {
|
||||
/* we dont use `language-` classes anymore so thats why we need to add background and text color manually */
|
||||
background: #2d2d2d;
|
||||
color: #ccc;
|
||||
|
||||
/* you must provide font-family font-size line-height. Example: */
|
||||
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
/* optional class for removing the outline */
|
||||
.prism-editor__textarea:focus {
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
20
ui/src/components/JSONHTML.vue
Normal file
20
ui/src/components/JSONHTML.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="(value, key) in json" :key="key">
|
||||
<dl v-if="typeof(value) == 'object'">
|
||||
<dt>{{ key }}</dt>
|
||||
<dd><JSONHTML :json="value"></JSONHTML></dd>
|
||||
</dl>
|
||||
<span v-else>
|
||||
<b>{{ key }}</b>: {{ value }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "JSONHTML",
|
||||
props: ["json"]
|
||||
}
|
||||
</script>
|
||||
153
ui/src/components/List.vue
Normal file
153
ui/src/components/List.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<v-row class="fill-height ma-0">
|
||||
<v-col cols="3" class="listnav" style="">
|
||||
<v-list nav color="background">
|
||||
<v-list-item
|
||||
v-if="showNew && canWrite"
|
||||
:to="{ name: routername, params: { id: 'new' } }"
|
||||
class="mt-4 mx-4 text-center newbutton"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
<v-icon small>mdi-plus</v-icon> New {{ singular }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-subheader class="pl-4">{{ plural }}</v-subheader>
|
||||
<v-list-item
|
||||
v-for="item in (items ? items : [])"
|
||||
:key="item[itemid]"
|
||||
link
|
||||
:to="{ name: routername, params: { id: item[itemid] } }"
|
||||
class="mx-2"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ item[itemname] }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
|
||||
<v-list-item-action v-if="deletable && canWrite">
|
||||
<v-icon @click="askDelete(item[itemid])" class="fader">
|
||||
mdi-close
|
||||
</v-icon>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-col>
|
||||
<v-col cols="9">
|
||||
<router-view></router-view>
|
||||
</v-col>
|
||||
<v-dialog v-model="dialog" persistent max-width="400">
|
||||
<v-card>
|
||||
<v-card-title> Delete {{ singular }} {{ deleteName }} ? </v-card-title>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="error" text @click="dialog = false">Cancel</v-btn>
|
||||
<v-btn color="success" outlined @click="deleteItem(deleteName)">Delete</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
interface State {
|
||||
dialog: boolean;
|
||||
deleteName?: string;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "List",
|
||||
components: { },
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
routername: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
itemid: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
itemname: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
singular: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
plural: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
showNew: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
deletable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
writepermission: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
data: (): State => ({
|
||||
dialog: false,
|
||||
deleteName: undefined,
|
||||
}),
|
||||
computed: {
|
||||
canWrite: function (): boolean {
|
||||
return this.hasRole(this.writepermission);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
askDelete(name: string) {
|
||||
this.deleteName = name;
|
||||
this.dialog = true;
|
||||
},
|
||||
deleteItem(deleteName: string) {
|
||||
this.$emit('delete', deleteName);
|
||||
this.dialog = false;
|
||||
},
|
||||
hasRole: function (s: string): boolean {
|
||||
if (this.$store.state.user.roles) {
|
||||
return this.lodash.includes(this.$store.state.user.roles, s);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.listnav {
|
||||
border-right: 1px solid #e0e0e0;
|
||||
}
|
||||
.theme--dark .listnav {
|
||||
border-right: 1px solid #393a3f;
|
||||
}
|
||||
|
||||
.newbutton {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
.theme--dark .newbutton {
|
||||
background: #424242;
|
||||
}
|
||||
|
||||
.v-list-item .fader {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
.v-list-item:hover .fader {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
</style>
|
||||
325
ui/src/components/TicketList.vue
Normal file
325
ui/src/components/TicketList.vue
Normal file
@@ -0,0 +1,325 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row v-if="type">
|
||||
<v-col cols="12">
|
||||
<v-btn elevation="0" rounded class="float-right" :to="{ name: 'TicketNew', params: { type: type } }">
|
||||
<v-icon class="mr-1">mdi-plus</v-icon>
|
||||
New {{ type | capitalize }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-toolbar
|
||||
rounded
|
||||
filled
|
||||
dense
|
||||
flat
|
||||
elevation="0"
|
||||
style="border-radius: 40px !important;"
|
||||
>
|
||||
<v-btn-toggle dense v-model="defaultcaql">
|
||||
<v-btn
|
||||
text
|
||||
color="primary"
|
||||
@click="caql = !caql"
|
||||
rounded
|
||||
style="border-radius: 40px !important;">
|
||||
CAQL
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
|
||||
<v-text-field
|
||||
placeholder="Search term or query (e.g. name == 'malware' AND 'wannacry')"
|
||||
v-model="term"
|
||||
dense
|
||||
solo
|
||||
flat
|
||||
hide-details
|
||||
clearable
|
||||
append-icon="mdi-magnify"
|
||||
@click:clear="clear"
|
||||
@click:append="loadTickets"
|
||||
@keydown.enter="loadTickets"
|
||||
:rules="[validate]"
|
||||
@focus="focus = true"
|
||||
@blur="blur"
|
||||
></v-text-field>
|
||||
</v-toolbar>
|
||||
|
||||
<span v-if="focus && caql">CAQL Query Suggestions</span>
|
||||
<v-list class="mb-2" v-if="focus && caql">
|
||||
<v-list-item v-for="example in examples" :key="example.q" dense link @click="term = example.q; caql = true; defaultcaql = 0; loadTickets()">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
<v-row>
|
||||
<span class="col-6">{{ example.q }}</span> <span class="text--disabled col-6">{{ example.desc }}</span>
|
||||
</v-row>
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item-content>
|
||||
Fields: {{ lodash.join(fields, ", ") }}
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="tickets"
|
||||
item-key="name"
|
||||
multi-sort
|
||||
class="elevation-0 cards clickable mt-2"
|
||||
:options.sync="options"
|
||||
:server-items-length="totalTickets"
|
||||
:loading="loading"
|
||||
:footer-props="{ 'items-per-page-options': [10, 25, 50, 100] }"
|
||||
>
|
||||
<template v-slot:item="{ item }">
|
||||
<tr @click="open(item)">
|
||||
<td colspan="5">
|
||||
<ticketSnippet :ticket="item" class="pa-0"></ticketSnippet>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import { Ticket } from "@/client";
|
||||
import { API } from "@/services/api";
|
||||
import TicketSnippet from "../components/snippets/TicketSnippet.vue";
|
||||
import {validateCAQL} from "@/suggestions/suggestions";
|
||||
import {DateTime} from "luxon";
|
||||
|
||||
interface State {
|
||||
term: string;
|
||||
loading: boolean;
|
||||
tickets: Array<Ticket>;
|
||||
totalTickets: number;
|
||||
options: {
|
||||
page?: number;
|
||||
itemsPerPage?: number;
|
||||
sortBy?: string[];
|
||||
sortDesc?: boolean[];
|
||||
groupBy?: string[];
|
||||
groupDesc?: boolean[];
|
||||
multiSort?: boolean;
|
||||
mustSort?: boolean;
|
||||
};
|
||||
focus: boolean;
|
||||
caql: boolean;
|
||||
defaultcaql?: number;
|
||||
}
|
||||
|
||||
interface QuerySuggestion {
|
||||
q: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "TicketList",
|
||||
components: {
|
||||
TicketSnippet,
|
||||
},
|
||||
props: ["type", "query"],
|
||||
data: (): State => ({
|
||||
term: "status == 'open'",
|
||||
loading: true,
|
||||
tickets: [],
|
||||
totalTickets: 0,
|
||||
options: {
|
||||
itemsPerPage: 10,
|
||||
},
|
||||
focus: false,
|
||||
caql: true,
|
||||
defaultcaql: 0,
|
||||
}),
|
||||
computed: {
|
||||
fields(): Array<string> {
|
||||
return [
|
||||
"type", "id", "name",
|
||||
"status" , "owner",
|
||||
"created", "modified",
|
||||
"details", "details.description", "details.…",
|
||||
"schema",
|
||||
"comments", "comments.#.created", "comments.#.creator", "comments.#.message",
|
||||
"playbooks", "playbooks.#.name", "playbooks.#.tasks",
|
||||
"references", "references.#.href", "references.#.name",
|
||||
"artifacts", "artifacts.#.name", "artifacts.#.status", "artifacts.#.type",
|
||||
"files", "files.#.name"
|
||||
];
|
||||
},
|
||||
user(): string {
|
||||
return this.$store.state.user.id
|
||||
},
|
||||
headers() {
|
||||
return [
|
||||
{
|
||||
text: "Name",
|
||||
align: "start",
|
||||
value: "name",
|
||||
},
|
||||
{
|
||||
text: "Status",
|
||||
align: "start",
|
||||
value: "status",
|
||||
},
|
||||
{
|
||||
text: "Owner",
|
||||
align: "start",
|
||||
value: "owner",
|
||||
},
|
||||
{
|
||||
text: "Creation",
|
||||
align: "start",
|
||||
value: "created",
|
||||
},
|
||||
{
|
||||
text: "Last Modification",
|
||||
align: "start",
|
||||
value: "modified",
|
||||
},
|
||||
];
|
||||
},
|
||||
examples (): Array<QuerySuggestion> {
|
||||
let twoWeeksAgo = DateTime.utc().minus({weeks: 2}).toFormat("yyyy-MM-dd");
|
||||
|
||||
let ex: Array<QuerySuggestion> = [];
|
||||
|
||||
if (this.user) {
|
||||
ex.push({q: "status == 'open' AND (owner == '" + this.user + "' OR !owner)", desc: "Select all open tickets by you and unassigned"})
|
||||
ex.push({q: "status == 'closed' AND owner == '" + this.user + "'", desc: "Select completed tickets by you"})
|
||||
} else {
|
||||
ex.push({q: "status == 'open'", desc: "Select all open tickets"})
|
||||
ex.push({q: "status == 'closed'", desc: "Select completed tickets"})
|
||||
}
|
||||
|
||||
ex.push({q: "created > \""+twoWeeksAgo+"\"", desc: "Select tickets created in the last two weeks"})
|
||||
|
||||
if (this.term && this.term.match(/^[A-Za-z ]+$/)) {
|
||||
ex.unshift({q: "'" + this.term + "'", desc: "Full text search for '" + this.term + "'"})
|
||||
}
|
||||
return ex;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
options: {
|
||||
handler() {
|
||||
this.loadTickets();
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
$route: function () {
|
||||
this.loadTickets();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
blur: function () {
|
||||
setTimeout(()=>{this.focus = false}, 200)
|
||||
},
|
||||
clear: function () {
|
||||
this.caql = false;
|
||||
this.defaultcaql = undefined;
|
||||
this.term = "";
|
||||
this.loadTickets();
|
||||
},
|
||||
open: function (ticket: Ticket) {
|
||||
this.$emit("click", ticket);
|
||||
},
|
||||
select: function (e: string) {
|
||||
this.loadTerm(e);
|
||||
},
|
||||
loadTickets() {
|
||||
let term = this.term;
|
||||
if (!term) {
|
||||
term = "";
|
||||
}
|
||||
this.loadTerm(term);
|
||||
},
|
||||
loadTerm(term: string) {
|
||||
this.loading = true;
|
||||
let offset = 0;
|
||||
let count = 25;
|
||||
let sortBy: Array<string> = [];
|
||||
let sortDesc: Array<boolean> = [];
|
||||
if (this.options.itemsPerPage !== undefined) {
|
||||
count = this.options.itemsPerPage;
|
||||
if (this.options.page !== undefined) {
|
||||
offset = (this.options.page - 1) * this.options.itemsPerPage;
|
||||
}
|
||||
}
|
||||
if (this.options.sortBy !== undefined) {
|
||||
sortBy = this.options.sortBy;
|
||||
}
|
||||
if (this.options.sortDesc !== undefined) {
|
||||
sortDesc = this.options.sortDesc;
|
||||
}
|
||||
|
||||
let t = this.type;
|
||||
if (!t) {
|
||||
t = "";
|
||||
}
|
||||
|
||||
if (!this.caql && this.term.length > 0) {
|
||||
term = "'" + this.lodash.join(this.lodash.split(term, " "), "'&&'") + "'"
|
||||
}
|
||||
|
||||
API.listTickets(t, offset, count, sortBy, sortDesc, term)
|
||||
.then((response) => {
|
||||
if (response.data.tickets) {
|
||||
this.tickets = response.data.tickets;
|
||||
} else {
|
||||
this.tickets = [];
|
||||
}
|
||||
this.totalTickets = response.data.count;
|
||||
this.loading = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
|
||||
},
|
||||
validate: function () {
|
||||
if (!this.term) {
|
||||
return true
|
||||
}
|
||||
let err = validateCAQL(this.term);
|
||||
if (err !== null) {
|
||||
return err.message;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.user) {
|
||||
this.term = "status == 'open' AND (owner == '" + this.user + "' OR !owner)";
|
||||
} else {
|
||||
this.term = "status == 'open'";
|
||||
}
|
||||
|
||||
if (this.query) {
|
||||
this.term = this.query;
|
||||
}
|
||||
|
||||
this.loadTickets();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.clickable td {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
.vue-simple-suggest.designed .input-wrapper input {
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
color: #fff;
|
||||
}
|
||||
.vue-simple-suggest.designed .suggestions {
|
||||
background-color: #333 !important;
|
||||
top: 60px !important;
|
||||
border: 1px solid #ddd !important;
|
||||
}
|
||||
</style>
|
||||
139
ui/src/components/Timeline.vue
Normal file
139
ui/src/components/Timeline.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-textarea
|
||||
v-model="comment"
|
||||
hide-details
|
||||
flat
|
||||
label="Add a comment..."
|
||||
solo
|
||||
auto-grow
|
||||
rows="2"
|
||||
class="py-2"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<v-btn class="mx-0 mt-n1" text @click="addTicketLog">
|
||||
<v-icon>mdi-send</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-textarea>
|
||||
<div
|
||||
v-for="(log, id) in internalLogs"
|
||||
:key="id"
|
||||
:icon="icon(log.type)"
|
||||
:small="small(log.type)"
|
||||
class="pb-2"
|
||||
>
|
||||
<v-card v-if="log.type === 'comment'" elevation="0" color="cards">
|
||||
<v-card-subtitle class="pb-0">
|
||||
<strong> {{ log.creator }}</strong>
|
||||
<span class="text--disabled ml-3" :title="log.created">
|
||||
{{ relDate(log.created) }}
|
||||
</span>
|
||||
</v-card-subtitle>
|
||||
<v-card-text class="mb-0 mt-2">
|
||||
<!--{{ log.message }}-->
|
||||
<vue-markdown v-if="show">{{ log.message }}</vue-markdown>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<div v-else style="line-height: 24px" class="d-flex flex-row">
|
||||
<v-divider class="mt-3 mr-3"></v-divider>
|
||||
{{ log.message }}
|
||||
<span class="text--disabled ml-1" :title="log.created">
|
||||
·
|
||||
{{ log.creator }} ·
|
||||
{{ relDate(log.created) }}
|
||||
</span>
|
||||
<v-divider class="mt-3 ml-3"></v-divider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import { LogEntry } from "@/client";
|
||||
import VueMarkdown from "vue-markdown";
|
||||
import { API } from "@/services/api";
|
||||
|
||||
interface State {
|
||||
comment: string
|
||||
internalLogs: Array<LogEntry>
|
||||
show: boolean
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "Timeline",
|
||||
components: {
|
||||
"vue-markdown": VueMarkdown
|
||||
},
|
||||
props: ["id", "logs"],
|
||||
data: (): State => ({
|
||||
comment: "",
|
||||
internalLogs: [],
|
||||
show: true
|
||||
}),
|
||||
watch: {
|
||||
logs: function () {
|
||||
// this.internalLogs = this.logs;
|
||||
this.reload(this.logs);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
reload: function(newlogs: Array<LogEntry>) {
|
||||
if (newlogs === undefined) {
|
||||
return
|
||||
}
|
||||
this.show = false;
|
||||
Vue.nextTick(() => {
|
||||
this.internalLogs = newlogs;
|
||||
this.show = true;
|
||||
})
|
||||
},
|
||||
icon: function(s: string) {
|
||||
switch (s) {
|
||||
case "comment":
|
||||
return "mdi-comment";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
small: function(s: string) {
|
||||
switch (s) {
|
||||
case "comment":
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
relDate: function(date: string) {
|
||||
let rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
||||
let deltaDays =
|
||||
(new Date(date).getTime() - new Date().getTime()) / (1000 * 3600 * 24);
|
||||
let relDate = rtf.format(Math.round(deltaDays), "days");
|
||||
if (deltaDays > -3) {
|
||||
relDate +=
|
||||
", " +
|
||||
new Date(date).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit"
|
||||
});
|
||||
}
|
||||
return relDate;
|
||||
},
|
||||
addTicketLog() {
|
||||
// API.addLog({ id: this.id, message: this.comment }).then(
|
||||
// response => {
|
||||
// this.$store.dispatch("alertSuccess", { name: "Log saved", type: "success" });
|
||||
// if (this.internalLogs === undefined) {
|
||||
// this.reload([response.data]);
|
||||
// } else {
|
||||
// this.internalLogs.unshift(response.data);
|
||||
// this.reload(this.internalLogs);
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.internalLogs = this.logs;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
173
ui/src/components/UserDataEditor.vue
Normal file
173
ui/src/components/UserDataEditor.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="editoruserdata === undefined" class="text-sm-center py-16">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
:size="70"
|
||||
:width="7"
|
||||
class="align-center"
|
||||
>
|
||||
</v-progress-circular>
|
||||
</div>
|
||||
<div v-else class="pa-8">
|
||||
<v-form>
|
||||
<v-row>
|
||||
<v-col cols="4" class="d-flex flex-column align-center">
|
||||
<v-avatar v-if="editoruserdata.image" size="128" class="mt-1">
|
||||
<img :src="editoruserdata.image" alt="userdata avatar" />
|
||||
</v-avatar>
|
||||
|
||||
<v-file-input
|
||||
v-model="file"
|
||||
type="file"
|
||||
class="pt-2 flex-grow-0"
|
||||
style="width: 100%"
|
||||
accept="image/png, image/jpeg"
|
||||
label="Select Image"
|
||||
@change="change"
|
||||
:clearable="false"
|
||||
>
|
||||
<template v-slot:append-outer>
|
||||
<v-btn
|
||||
v-if="showCrop"
|
||||
rounded
|
||||
small
|
||||
color="accent"
|
||||
@click="validate"
|
||||
>
|
||||
<v-icon>
|
||||
mdi-check
|
||||
</v-icon>
|
||||
Set
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="!!editoruserdata.image"
|
||||
small
|
||||
rounded
|
||||
color="error"
|
||||
@click="
|
||||
file = null;
|
||||
editoruserdata.image = '';
|
||||
showCrop = false;
|
||||
"
|
||||
class="ml-2"
|
||||
>
|
||||
<v-icon>
|
||||
mdi-close
|
||||
</v-icon>
|
||||
Clear
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-file-input>
|
||||
|
||||
<vue-cropper
|
||||
v-if="showCrop"
|
||||
ref="cropper"
|
||||
v-bind="{ aspectRatio: 1, autoCrop: true }"
|
||||
:src="imgSrc"
|
||||
alt="Avatar"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-text-field
|
||||
prepend-icon="mdi-account"
|
||||
label="Name"
|
||||
v-model="editoruserdata.name"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
prepend-icon="mdi-email"
|
||||
label="Email"
|
||||
v-model="editoruserdata.email"
|
||||
></v-text-field>
|
||||
|
||||
<v-btn
|
||||
color="success"
|
||||
outlined
|
||||
@click="saveUserData"
|
||||
class="mt-6"
|
||||
>
|
||||
<v-icon>mdi-content-save</v-icon>
|
||||
Save
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import { UserData } from "@/client";
|
||||
import VueCropper from "vue-cropperjs";
|
||||
import "cropperjs/dist/cropper.css";
|
||||
|
||||
interface State {
|
||||
tab: number;
|
||||
editoruserdata?: UserData;
|
||||
file: File | null;
|
||||
imgSrc: string | ArrayBuffer | null;
|
||||
showCrop: boolean;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "UserDataEditor",
|
||||
data: (): State => ({
|
||||
tab: 0,
|
||||
editoruserdata: undefined,
|
||||
file: null,
|
||||
imgSrc: null,
|
||||
showCrop: false
|
||||
}),
|
||||
props: ['userdata'],
|
||||
components: {
|
||||
VueCropper
|
||||
},
|
||||
methods: {
|
||||
saveUserData: function() {
|
||||
this.$emit("save", this.editoruserdata);
|
||||
},
|
||||
change: function() {
|
||||
if (!this.file) {
|
||||
this.imgSrc = null;
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = ticket => {
|
||||
if (ticket.target && this.$refs.cropper) {
|
||||
this.imgSrc = ticket.target.result;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let cropper: any = this.$refs.cropper;
|
||||
cropper.replace(this.imgSrc);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(this.file);
|
||||
this.showCrop = true;
|
||||
},
|
||||
validate: function() {
|
||||
if (this.$refs.cropper && this.editoruserdata) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let cropper: any = this.$refs.cropper;
|
||||
this.editoruserdata.image = cropper
|
||||
.getCroppedCanvas({width: 128, height: 128})
|
||||
.toDataURL("image/png");
|
||||
// this.on.input(croppedImg)
|
||||
this.showCrop = false;
|
||||
// this.file = null
|
||||
// this.imgSrc = null
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.editoruserdata = this.userdata;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.theme--dark.v-tabs-items,
|
||||
.theme--dark.v-tabs > .v-tabs-bar {
|
||||
background: none;
|
||||
}
|
||||
</style>
|
||||
85
ui/src/components/VJsfCropImg.vue
Normal file
85
ui/src/components/VJsfCropImg.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<v-input :value="value" class="vjsf-crop-img">
|
||||
<v-row class="mt-0 mx-0" align="center">
|
||||
<v-avatar v-if="value" size="128" class="mt-1">
|
||||
<img :src="value">
|
||||
</v-avatar>
|
||||
<v-file-input
|
||||
v-model="file"
|
||||
type="file"
|
||||
class="pt-2"
|
||||
accept="image/png, image/jpeg"
|
||||
placeholder="User Avatar"
|
||||
@change="change"
|
||||
:clearable="false"
|
||||
>
|
||||
<template v-slot:append-outer>
|
||||
<v-btn v-if="imgSrc" fab x-small color="accent" @click="validate">
|
||||
<v-icon>
|
||||
mdi-check
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-file-input>
|
||||
<v-icon v-if="!!value" @click="on.input(null)">
|
||||
mdi-close
|
||||
</v-icon>
|
||||
</v-row>
|
||||
<vue-cropper
|
||||
v-if="file"
|
||||
ref="cropper"
|
||||
v-bind="cropperOptions"
|
||||
:src="imgSrc"
|
||||
alt="Avatar"
|
||||
/>
|
||||
</v-input>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VueCropper from 'vue-cropperjs'
|
||||
import 'cropperjs/dist/cropper.css'
|
||||
|
||||
export default {
|
||||
components: { VueCropper },
|
||||
props: {
|
||||
value: { type: String, default: '' },
|
||||
on: { type: Object, required: true },
|
||||
cropperOptions: { type: Object, default: () => ({ aspectRatio: 1, autoCrop: true }) },
|
||||
size: { type: Number, default: 128 } // same as default v-avatar size
|
||||
},
|
||||
data: () => ({
|
||||
file: null,
|
||||
imgSrc: null
|
||||
}),
|
||||
computed: {},
|
||||
methods: {
|
||||
change() {
|
||||
if (!this.file) {
|
||||
this.imgSrc = null
|
||||
return
|
||||
}
|
||||
const reader = new FileReader()
|
||||
reader.onload = (ticket) => {
|
||||
this.imgSrc = ticket.target.result
|
||||
this.dialog = true
|
||||
this.$refs.cropper.replace(this.imgSrc)
|
||||
}
|
||||
reader.readAsDataURL(this.file)
|
||||
},
|
||||
async validate() {
|
||||
const croppedImg = this.$refs.cropper
|
||||
.getCroppedCanvas({ width: this.size, height: this.size })
|
||||
.toDataURL('image/png')
|
||||
this.on.input(croppedImg)
|
||||
this.file = null
|
||||
this.imgSrc = null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
.vjsf-crop-img>.v-input__control>.v-input__slot {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
20
ui/src/components/charts/Bar.ts
Normal file
20
ui/src/components/charts/Bar.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Component, Mixins } from 'vue-property-decorator'
|
||||
import {HorizontalBar, mixins} from 'vue-chartjs';
|
||||
import ChartOptions from "chart.js";
|
||||
|
||||
@Component({
|
||||
extends: HorizontalBar,
|
||||
mixins: [mixins.reactiveProp],
|
||||
props: {
|
||||
chartOptions: {
|
||||
type: ChartOptions,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
})
|
||||
export default class BarChart extends Mixins(mixins.reactiveProp, HorizontalBar) {
|
||||
mounted () {
|
||||
// @ts-expect-error chartOptions are not expected
|
||||
this.renderChart(this.chartData, this.chartOptions);
|
||||
}
|
||||
}
|
||||
20
ui/src/components/charts/Doughnut.ts
Normal file
20
ui/src/components/charts/Doughnut.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Component, Mixins } from "vue-property-decorator";
|
||||
import { mixins, Pie } from "vue-chartjs";
|
||||
import ChartOptions from "chart.js";
|
||||
|
||||
@Component({
|
||||
extends: Pie,
|
||||
mixins: [mixins.reactiveProp],
|
||||
props: {
|
||||
chartOptions: {
|
||||
type: ChartOptions,
|
||||
default: null
|
||||
}
|
||||
}
|
||||
})
|
||||
export default class DoughnutChart extends Mixins(mixins.reactiveProp, Pie) {
|
||||
mounted() {
|
||||
// @ts-expect-error chartOptions are not expected
|
||||
this.renderChart(this.chartData, this.chartOptions);
|
||||
}
|
||||
}
|
||||
20
ui/src/components/charts/Line.ts
Normal file
20
ui/src/components/charts/Line.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Component, Mixins } from 'vue-property-decorator'
|
||||
import {Line, mixins} from 'vue-chartjs';
|
||||
import ChartOptions from "chart.js";
|
||||
|
||||
@Component({
|
||||
extends: Line,
|
||||
mixins: [mixins.reactiveProp],
|
||||
props: {
|
||||
chartOptions: {
|
||||
type: ChartOptions,
|
||||
default: null,
|
||||
}
|
||||
},
|
||||
})
|
||||
export default class LineChart extends Mixins(mixins.reactiveProp, Line) {
|
||||
mounted () {
|
||||
// @ts-expect-error chartOptions are not expected
|
||||
this.renderChart(this.chartData, this.chartOptions);
|
||||
}
|
||||
}
|
||||
34
ui/src/components/icons/ArangoIcon.vue
Normal file
34
ui/src/components/icons/ArangoIcon.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<svg
|
||||
width="64"
|
||||
viewBox="74.499 105.376 55.861 54.285"
|
||||
height="64"
|
||||
>
|
||||
<path
|
||||
style="fill: #ffffff"
|
||||
id="path1566"
|
||||
fill="#577138"
|
||||
d="m128.867 130.714c-1.045-3.482-3.378-6.163-5.467-9.02-3.97-5.362-9.123-8.322-13.86-8.357-9.088-.174-15.495 3.97-18.246 10.48-.348.836-.8 1.184-1.637 1.358-3.03.696-6.06 1.393-8.88 2.82-1.915.975-3.865 1.88-5.084 3.76-1.532 2.368-1.358 4.875-.766 7.487 1.532 6.476 5.467 10.48 11.978 11.943 4.005.87 7.904.766 11.56-1.323 2.298-1.288 4.144-3.133 5.955-5.015.453-.487.8-.592 1.393-.313l3.343 1.637c3.552 1.776 7.173 3.238 11.247 2.994 6.477-.383 10.342-4.736 9.924-11.073-.14-2.507-.73-4.944-1.463-7.382zm-23.992 8.8c-3.9 2.6-7.94 4.875-12.605 5.815-1.497.314-2.995.418-4.005.348-3.76 0-6.93-.382-9.715-2.298-3.273-2.3-4.666-7.313-2.786-10.83.592-1.15 1.602-1.985 2.716-2.647 3.865-2.403 8.183-3.517 12.57-4.422 4.56-.975 9.192-2.02 13.963-1.985 2.298 0 4.352.905 5.815 2.75 1.288 1.67.906 3.552.28 5.328-1.22 3.343-3.308 6-6.233 7.94zm23.713 2.856c-1.463 3.656-4.562 4.875-8.078 5.397-3.76.557-6.86-1.22-10.063-2.716-1.045-.487-2.055-1.114-3.134-1.532-.94-.348-1.08-.697-.313-1.358 1.184-1.497 2.368-2.995 3.308-4.667 1.15-2.472 1.846-5.084 1.846-7.834 0-4.248-2.264-6.13-6.546-6.616-.836-.104-1.706-.035-2.577-.035-2.9.2-5.78.662-8.636 1.184-.592-.035-1.323.662-1.706.035-.278-.453.418-1 .73-1.497 2.1-3.62 5.223-5.85 9.158-7.104 3.447-1.08 6.86-1.323 10.4-.383 2.925.766 5.014 2.542 6.93 4.666 2.368 2.6 4.527 5.328 6.268 8.357 1.706 2.96 2.577 6.128 2.82 9.506.07 1.566.2 3.1-.418 4.597z"
|
||||
/>
|
||||
<path
|
||||
style="fill: #ffffff"
|
||||
id="path1570"
|
||||
fill="#a2b24f"
|
||||
d="m126.185 128.277c-1.776-3.03-3.935-5.78-6.268-8.357-1.915-2.124-4.005-3.9-6.93-4.666-3.55-.94-6.964-.697-10.4.383-3.935 1.22-7.07 3.482-9.158 7.104-.278.487-1 1.045-.73 1.497.383.592 1.114-.07 1.706-.035.696-2.263 2.228-3.935 4.144-5.153 3.378-2.194 7.138-3.1 11.178-3.03 3.9.104 6.964 1.8 9.54 4.63 1.88 2.054 3.517 4.353 5.12 6.616 2.542 3.587 3.935 7.556 3.656 12.013-.2 3.17-1.67 5.537-4.77 6.686-2.542.94-5.12 1.15-7.695.035-1.497-.627-3.065-1.08-4.527-1.8-1.358-.66-2.925-.836-4.04-1.95-.8.66-.627 1 .313 1.358 1.08.417 2.1 1.045 3.134 1.532 3.204 1.497 6.337 3.273 10.063 2.716 3.517-.522 6.616-1.775 8.078-5.397.592-1.497.488-3.03.383-4.56-.2-3.482-1.08-6.65-2.786-9.6zm-15.112 3.308c.627-1.776 1-3.656-.28-5.328-1.462-1.88-3.482-2.75-5.815-2.75-4.736-.035-9.367 1-13.998 1.985-4.387.94-8.705 2.02-12.57 4.422-1.114.697-2.124 1.532-2.716 2.647-1.845 3.517-.487 8.53 2.786 10.83 2.786 1.95 5.954 2.298 9.715 2.298 1 .07 2.542-.07 4.004-.348 4.666-.94 8.705-3.204 12.605-5.815 2.994-1.95 5.084-4.596 6.268-7.94zm-8.044 8.183c-3.308 1.985-6.65 3.9-10.516 4.596-3.134.557-6.338.732-9.47-.07-3.656-.87-6.477-4.18-6.616-7.94-.07-2.055.94-3.726 2.507-5.05 2.438-2.054 5.328-3.134 8.392-3.796.07 0 .174.104.278.174 2.577-1.985 5.502-2.368 8.636-2.124.278-.66.94-.453 1.428-.592 2.68-.662 5.397-.94 8.078-.383 5.05 1 5.676 3.83 4.04 7.94-1.288 3.308-3.83 5.467-6.755 7.243zm8.6-2.508c3.308.2 5.64-2.9 5.64-5.57 0-3.726-1.985-6.407-4.84-8.53-1.358-.975-2.9-1.636-4.562-1.045-1.428.487-2.82.8-4.318.697-.174 0-.348.14-.522.174.87 0 1.706-.07 2.577.034 4.283.488 6.546 2.368 6.546 6.616 0 2.75-.696 5.362-1.846 7.835.383-.245.836-.245 1.323-.2z"
|
||||
/>
|
||||
<path
|
||||
style="fill: #ffffff"
|
||||
id="path1574"
|
||||
fill="#5e3108"
|
||||
d="m83.564 134.823c-.105.905.14 1.636 1 2.16 3.134 1.915 6.477 3.03 10.203 2.6 2.75-.313 5.327-1.184 7.452-3.03 1.985-1.706 2.368-3.83 1.044-6.024-1.637-2.68-3.97-4.353-7.034-4.944-3.1-.244-6.06.14-8.636 2.124-2.228 1.88-3.726 4.18-4.04 7.104z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ArangoIcon",
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
31
ui/src/components/icons/EmitterIcon.vue
Normal file
31
ui/src/components/icons/EmitterIcon.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<svg viewBox="0 0 205.18876 205.19" height="205.19" width="205.18875" id="svg4136">
|
||||
<g transform="matrix(1.25,0,0,-1.25,0,205.19)" id="g4144">
|
||||
<g transform="scale(0.1)" style="fill: #ff0000" clip-path="url(#clipPath4152)" id="g4150">
|
||||
<path
|
||||
id="path4156"
|
||||
style="fill: #ffffff; fill-opacity: 1; fill-rule: nonzero; stroke: none"
|
||||
d="m 820.992,276.18 c -295.238,0 -535.441,240.211 -535.441,535.465 0,295.245 240.203,535.445 535.441,535.445 295.258,0 535.468,-240.2 535.468,-535.445 0,-58.926 -47.77,-106.696 -106.71,-106.696 -58.92,0 -106.69,47.77 -106.69,106.696 0,177.574 -144.482,322.055 -322.068,322.055 -177.574,0 -322.047,-144.481 -322.047,-322.055 0,-177.575 144.473,-322.075 322.047,-322.075 58.93,0 106.703,-47.769 106.703,-106.691 0,-58.93 -47.773,-106.699 -106.703,-106.699"
|
||||
/>
|
||||
<path
|
||||
id="path4158"
|
||||
style="fill: #ffffff; fill-opacity: 1; fill-rule: nonzero; stroke: none"
|
||||
d="m 1009.77,811.645 c 0,-104.254 -84.516,-188.774 -188.778,-188.774 -104.258,0 -188.769,84.52 -188.769,188.774 0,104.257 84.511,188.775 188.769,188.775 104.262,0 188.778,-84.518 188.778,-188.775"
|
||||
/>
|
||||
<path
|
||||
id="path4160"
|
||||
style="fill: #ffffff; fill-opacity: 1; fill-rule: nonzero; stroke: none"
|
||||
d="m 820.758,0 c -45.328,0 -82.074,36.7617 -82.074,82.0781 0,45.3319 36.746,82.0629 82.074,82.0629 362.052,0 656.592,294.57 656.592,656.625 0,362.054 -294.54,656.604 -656.592,656.604 -362.059,0 -656.602,-294.55 -656.602,-656.604 0,-45.325 -36.758,-82.075 -82.0857,-82.075 C 36.7461,738.691 0,775.441 0,820.766 c 0,452.564 368.18,820.754 820.758,820.754 452.562,0 820.752,-368.19 820.752,-820.754 C 1641.51,368.191 1273.32,0 820.758,0"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "EmitterIcon",
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
22
ui/src/components/icons/MinioIcon.vue
Normal file
22
ui/src/components/icons/MinioIcon.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<svg
|
||||
viewBox="-0.231 -10.008 1143.859 1147.992"
|
||||
width="2500"
|
||||
height="2500"
|
||||
>
|
||||
<path
|
||||
style="fill: #ffffff"
|
||||
id="path887"
|
||||
fill="#f8e6ea"
|
||||
d="m 649.50958,1130.9299 c -1.375,-0.74 -16.788,-15.67 -34.25,-33.18 l -31.75,-31.834 V 858.86783 c 0,-113.876 -0.289,-207.336 -0.641,-207.69 -0.649,-0.647 -52.692,25.508 -205.773,103.415 -46.152,23.488 -84.548,42.462 -85.324,42.164 -0.906,-0.347 -1.206,-1.359 -0.837,-2.827 0.315,-1.257 1.018,-6.483 1.562,-11.614 2.754,-25.985 23.256,-87.028 45.006,-134 59.272,-128.003 155.174,-234.087 277.19,-306.617 19.913,-11.837 36.64,-20.494 38.337,-19.843 1.294,0.497 1.48,13.97 1.48,107.1 0,76.287 0.31,106.531 1.091,106.531 0.6,0 5.174,-2.109 10.165,-4.686 35.003,-18.079 60.409,-51.848 68.9,-91.58 2.871,-13.436 3.13,-37.923 0.537,-50.916 -4.763,-23.874 -15.756,-46.334 -31.252,-63.852 -7.652,-8.65 -33.057,-35.365 -77.946,-81.966 -67.875,-70.462 -80.303,-83.931 -88.255,-95.65 -5.834,-8.6 -10.87,-19.113 -13.922,-29.062 -2.447,-7.979 -2.7,-10.307 -2.754,-25.288003 -0.065,-18.287 0.974,-23.978 6.985,-38.248 11.6,-27.537 41.822,-53.2229997 66.951,-56.9029997 17.627,-2.58 39.898,-2.083 54.386,1.217 11.81,2.69 30.942,14.2249997 44.057,26.5619997 15.396,14.484 23.147,26.045 81.04,120.872003 68.315,111.898 76.461,125.692 77.628,131.443 1.155,5.692 0.319,8.097 -2.79,8.027 -2.589,-0.058 -1.805,0.745 -124.8,-127.97 -10.774,-11.275 -29.537,-31.075 -41.694,-44 -31.729,-33.731003 -44.894,-46.440003 -50.863,-49.098003 -7.641,-3.404 -18.242,-3.248 -25.625,0.378 -14.476,7.109 -21.328,23.505 -15.867,37.970003 2.708,7.171 -1.064,3.061 87.873,95.75 39.844,41.525 75.834,79.55 79.979,84.5 66.885,79.884 61.447,198.583 -12.46,271.993 -16.97,16.855 -33.934,28.403 -65.364,44.493 -10.725,5.49 -20.962,10.883 -22.75,11.984 l -3.25,2 v 257.934 c 0,141.86307 -0.273,258.64407 -0.607,259.51407 -0.703,1.833 -1.03,1.835 -4.393,0.024 z m -177.63,-500.38507 c 87.548,-44.936 105.617,-54.442 108.183,-56.912 1.61,-1.548 2.312,-3.78 2.792,-8.857 0.969,-10.263 0.782,-118.091 -0.206,-118.702 -2.176,-1.345 -38.95,31.338 -63.639,56.56 -42.682,43.6 -78.365,92.834 -107.7,148.595 -3.836,7.292 -6.656,13.259 -6.267,13.259 0.389,0 30.467,-15.274 66.838,-33.943 z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "MinioIcon",
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
43
ui/src/components/icons/NodeRedIcon.vue
Normal file
43
ui/src/components/icons/NodeRedIcon.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<svg
|
||||
viewBox="0 0 512 512"
|
||||
width="512"
|
||||
height="512"
|
||||
>
|
||||
<g id="g861" transform="translate(0 -540.36)">
|
||||
<g style="fill: #ffffff" id="g841" transform="matrix(6.1195921,0,0,6.1195921,-464.85522,-1122.4443)">
|
||||
<path
|
||||
id="path839"
|
||||
fill="#ffffff"
|
||||
d="m 120.6731,296.55103 c -2.3846,0 -4.3789,2.0376 -4.3789,4.4222 v 0.77344 c -2.2304,0.10995 -4.0388,0.5467 -5.3281,1.4316 -1.5951,1.0948 -2.4675,2.582 -3.1816,3.877 -0.71415,1.295 -1.2996,2.4093 -2.1504,3.1894 -0.72545,0.66525 -1.7997,1.1216 -3.4512,1.3301 -0.21359,-2.192 -2.0615,-3.9512 -4.305197,-3.9512 h -17.167 c -2.3846,0 -4.4165,1.965 -4.4165,4.3496 v 4.2422 c 0,2.3846 2.0319,4.3516 4.4165,4.3516 h 17.167 c 2.384597,0 4.387197,-1.9669 4.387197,-4.3516 v -1.6133 c 9.7257,0.15307 12.467,2.6032 15.594,5.3379 3.0006,2.6241 6.6658,5.3789 15.436,5.4948 v 0.73633 c 0,2.3846 2.0695,4.3799 4.4541,4.3799 h 17.092 c 2.3846,0 4.4541,-1.9952 4.4541,-4.3799 v -4.2422 c 0,-2.3846 -2.0695,-4.3779 -4.4541,-4.3779 h -17.092 c -2.3846,0 -4.4541,1.9933 -4.4541,4.3779 v 0.58594 c -8.0984,-0.0599 -10.486,-2.1557 -13.498,-4.7897 -2.5035,-2.1893 -5.6398,-4.5852 -11.947,-5.5859 1.176,-1.1795 1.8834,-2.5192 2.5137,-3.6621 0.67724,-1.228 1.2899,-2.2006 2.2695,-2.873 0.76303,-0.52371 1.9757,-0.83922 3.6621,-0.93945 v 0.55078 c 0,2.3846 1.9943,4.3356 4.3789,4.3356 h 17.242 c 2.3846,0 4.3789,-1.951 4.3789,-4.3356 v -4.2422 c 0,-2.3846 -1.9943,-4.4222 -4.3789,-4.4222 z m 0,3 h 17.242 c 0.80513,0 1.3789,0.6171 1.3789,1.4222 v 4.2422 c 0,0.80514 -0.57378,1.3356 -1.3789,1.3356 h -17.242 c -0.80513,0 -1.3789,-0.53045 -1.3789,-1.3356 v -4.2422 c 0,-0.80513 0.57378,-1.4222 1.3789,-1.4222 z m -39.961997,11.016 h 17.167 c 0.80513,0 1.4165,0.60112 1.4165,1.4062 v 4.2422 c 0,0.80513 -0.61138,1.4082 -1.4165,1.4082 h -17.167 c -0.80513,0 -1.4165,-0.60307 -1.4165,-1.4082 v -4.2422 c 0,-0.80513 0.61137,-1.4062 1.4165,-1.4062 z m 57.037997,9.984 h 17.092 c 0.80513,0 1.4541,0.5728 1.4541,1.3779 v 4.168 c 0,0.80513 -0.64898,1.4541 -1.4541,1.4541 h -17.092 c -0.80513,0 -1.4541,-0.64897 -1.4541,-1.4541 v -4.168 c 0,-0.80513 0.64897,-1.3779 1.4541,-1.3779 z"
|
||||
style="
|
||||
color: #000000;
|
||||
text-indent: 0;
|
||||
text-decoration: none;
|
||||
text-decoration-line: none;
|
||||
text-decoration-style: solid;
|
||||
text-decoration-color: #000000;
|
||||
text-transform: none;
|
||||
white-space: normal;
|
||||
isolation: auto;
|
||||
mix-blend-mode: normal;
|
||||
solid-color: #000000;
|
||||
fill: #ffffff;
|
||||
color-rendering: auto;
|
||||
image-rendering: auto;
|
||||
shape-rendering: auto;
|
||||
"
|
||||
/>
|
||||
</g>
|
||||
<g id="g859" fill="#8f0000" transform="matrix(1.0024 0 0 1.0024 .26914 -2.6855)" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "NodeRedIcon",
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
64
ui/src/components/snippets/ArtifactSnippet.vue
Normal file
64
ui/src/components/snippets/ArtifactSnippet.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<v-list-item link dense>
|
||||
<v-list-item-content @click="goto">
|
||||
<v-list-item-title class="d-flex">
|
||||
<v-icon small class="mr-1">mdi-gauge</v-icon>
|
||||
<span class="text-truncate">{{ artifact.name }}</span>
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="d-flex">
|
||||
<v-icon small class="mr-1" :color="statusColor">{{ statusIcon }}</v-icon>
|
||||
<span :class="statusColor + '--text'">{{ artifact.status | capitalize }}</span>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-icon small class="mr-1">mdi-information</v-icon>
|
||||
<span class="mr-1">{{ artifact.enrichments ? lodash.size(artifact.enrichments) : 0 }}</span>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action v-if="action !== ''">
|
||||
<v-btn icon small>
|
||||
<v-icon small @click="actionClick">{{ action }}</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import {Type, TypeColorEnum} from "../../client";
|
||||
|
||||
export default Vue.extend({
|
||||
name: "ArtifactSnippet",
|
||||
props: ["artifact", "to", "action"],
|
||||
computed: {
|
||||
statusIcon: function () {
|
||||
let icon = "mdi-help";
|
||||
this.lodash.forEach(this.$store.state.settings.artifactStates, (state: Type) => {
|
||||
if (this.artifact.status === state.id) {
|
||||
icon = state.icon;
|
||||
}
|
||||
})
|
||||
return icon;
|
||||
},
|
||||
statusColor: function () {
|
||||
let color = TypeColorEnum.Info;
|
||||
this.lodash.forEach(this.$store.state.settings.artifactStates, (state: Type) => {
|
||||
if (this.artifact.status === state.id && state.color) {
|
||||
color = state.color
|
||||
}
|
||||
})
|
||||
return color;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
actionClick: function () {
|
||||
this.$emit("actionClick", this.artifact)
|
||||
},
|
||||
goto: function () {
|
||||
this.$emit("click", this.artifact)
|
||||
if (this.to) {
|
||||
this.$router.push(this.to);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
60
ui/src/components/snippets/IDSnippet.vue
Normal file
60
ui/src/components/snippets/IDSnippet.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div>
|
||||
<TicketSnippet v-if="ticket !== undefined" :ticket="ticket" :to="{ name: 'Ticket', params: { type: ticket.type, id: ticket.id } }"></TicketSnippet>
|
||||
<ArtifactSnippet v-if="artifact !== undefined" :artifact="artifact" :to="{ name: 'Artifact', params: { artifact: artifact.name } }"></ArtifactSnippet>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import {Artifact, TicketResponse} from "../../client";
|
||||
import {API} from "@/services/api";
|
||||
import TicketSnippet from "./TicketSnippet.vue";
|
||||
import ArtifactSnippet from "./ArtifactSnippet.vue";
|
||||
|
||||
interface State {
|
||||
ticket?: TicketResponse;
|
||||
artifact?: Artifact;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "IDSnippet",
|
||||
props: ["id"],
|
||||
data: (): State => ({
|
||||
ticket: undefined,
|
||||
artifact: undefined,
|
||||
}),
|
||||
components: {
|
||||
ArtifactSnippet,
|
||||
TicketSnippet
|
||||
},
|
||||
methods: {
|
||||
loadSnippet() {
|
||||
if (this.id.startsWith("tickets/")) {
|
||||
this.artifact = undefined;
|
||||
let ticketID = this.id.replace("tickets/", "")
|
||||
API.getTicket(ticketID).then(response => {
|
||||
this.ticket = response.data;
|
||||
});
|
||||
}
|
||||
if (this.id.startsWith("artifacts/")) {
|
||||
this.ticket = undefined;
|
||||
// TODO
|
||||
// let artifactID = this.id.replace("artifacts/", "")
|
||||
// API.getArtifact(artifactID).then(response => {
|
||||
// this.artifact = response.data;
|
||||
// });
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"id": "loadSnippet",
|
||||
$route: "loadSnippet"
|
||||
},
|
||||
mounted() {
|
||||
this.loadSnippet();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
91
ui/src/components/snippets/TicketSnippet.vue
Normal file
91
ui/src/components/snippets/TicketSnippet.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<v-list-item link dense>
|
||||
<v-list-item-content @click="goto">
|
||||
<v-list-item-title class="d-flex">
|
||||
<v-icon small class="mr-1">{{ typeIcon }}</v-icon>
|
||||
<span class="text-truncate">{{ ticket.type | capitalize }} #{{ ticket.id }}: {{ ticket.name }}</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-icon small class="mr-1">mdi-calendar-plus</v-icon>
|
||||
{{ ticket.created | formatdate($store.state.settings.timeformat) }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="d-flex">
|
||||
<v-icon small class="mr-1" :color="statusColor">{{ statusIcon }}</v-icon>
|
||||
<span :class="statusColor + '--text'">{{ ticket.status | capitalize }}</span>
|
||||
|
||||
<v-icon small class="mx-1">mdi-account</v-icon>
|
||||
{{ ticket.owner ? ticket.owner : 'unassigned' }}
|
||||
<v-spacer></v-spacer>
|
||||
<v-icon small class="mr-1">mdi-source-branch</v-icon>
|
||||
<span class="mr-1">{{ ticket.playbooks ? lodash.size(ticket.playbooks) : 0 }}</span>
|
||||
<v-icon small class="mr-1">mdi-checkbox-multiple-marked-outline</v-icon>
|
||||
<span class="mr-1">{{ opentaskcount }}</span>
|
||||
<v-icon small class="mr-1">mdi-comment</v-icon>
|
||||
<span class="mr-1">{{ ticket.comments ? ticket.comments.length : 0 }}</span>
|
||||
<v-icon small class="mr-1">mdi-file</v-icon>
|
||||
<span class="mr-1">{{ ticket.files ? ticket.files.length : 0 }}</span>
|
||||
<v-icon small class="mr-1">mdi-link</v-icon>
|
||||
<span class="mr-1">{{ ticket.references ? ticket.references.length : 0 }}</span>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action v-if="action !== ''">
|
||||
<v-btn icon small>
|
||||
<v-icon small @click="actionClick">{{ action }}</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import {Playbook, Task, Type, TypeColorEnum} from "@/client";
|
||||
|
||||
export default Vue.extend({
|
||||
name: "TicketSnippet",
|
||||
props: ["ticket", "to", "action"],
|
||||
computed: {
|
||||
opentaskcount: function() {
|
||||
let count = 0;
|
||||
this.lodash.forEach(this.ticket.playbooks, (playbook: Playbook) => {
|
||||
this.lodash.forEach(playbook.tasks, (task: Task) => {
|
||||
if (task.done) {
|
||||
count++;
|
||||
}
|
||||
})
|
||||
})
|
||||
return count;
|
||||
},
|
||||
typeIcon: function () {
|
||||
let icon = "mdi-help";
|
||||
this.lodash.forEach(this.$store.state.settings.ticketTypes, (ticketType: Type) => {
|
||||
if (this.ticket.type === ticketType.id) {
|
||||
icon = ticketType.icon
|
||||
}
|
||||
})
|
||||
return icon;
|
||||
},
|
||||
statusIcon: function() {
|
||||
if (this.ticket.status === 'closed') {
|
||||
return "mdi-checkbox-marked-circle-outline";
|
||||
}
|
||||
return "mdi-arrow-right-drop-circle-outline";
|
||||
},
|
||||
statusColor: function() {
|
||||
if (this.ticket.status === 'closed') {
|
||||
return TypeColorEnum.Success;
|
||||
}
|
||||
return TypeColorEnum.Info;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
actionClick: function () {
|
||||
this.$emit("actionClick", this.ticket)
|
||||
},
|
||||
goto: function () {
|
||||
if (this.to === undefined) {
|
||||
return
|
||||
}
|
||||
this.$router.push(this.to);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user