mirror of
https://github.com/SecurityBrewery/catalyst.git
synced 2025-12-06 23:32:47 +01:00
Release catalyst
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user