mirror of
https://github.com/SecurityBrewery/catalyst.git
synced 2026-04-24 19:47:48 +02:00
refactor: improve setup and maintainability (#1067)
This commit is contained in:
@@ -1,48 +0,0 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-card style="background-color: #fff;" class="mt-8">
|
||||
<div class="swagger" id="swagger" ></div>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import SwaggerUI from 'swagger-ui';
|
||||
import 'swagger-ui/dist/swagger-ui.css';
|
||||
import Vue from "vue";
|
||||
import spec from '../../../generated/community.json';
|
||||
|
||||
export default Vue.extend({
|
||||
name: "API",
|
||||
components: {},
|
||||
mounted() {
|
||||
SwaggerUI({
|
||||
spec: spec,
|
||||
dom_id: '#swagger',
|
||||
oauth2RedirectUrl: location.href
|
||||
})
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#swagger a,
|
||||
#swagger h1,
|
||||
#swagger h2,
|
||||
#swagger h3,
|
||||
#swagger h4,
|
||||
#swagger h5,
|
||||
#swagger h6 {
|
||||
color: black !important
|
||||
}
|
||||
|
||||
#swagger .info {
|
||||
padding: 0 10px 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#swagger .servers-title,
|
||||
#swagger .servers {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,325 +0,0 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<h1>{{ name }}</h1>
|
||||
|
||||
<v-divider class="my-2"></v-divider>
|
||||
|
||||
<h2 class="text--disabled" style="font-size: 12pt" v-if="artifact">
|
||||
Status:
|
||||
<v-menu offset-y class="mr-2">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<span v-bind="attrs" v-on="on">
|
||||
<v-icon small class="mr-1" :color="statusColor(artifact.status)">{{ statusIcon(artifact.status) }}</v-icon>
|
||||
<span :class="statusColor(artifact.status) + '--text'">{{ artifact.status | capitalize }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item dense link v-for="state in otherStates" :key="state.id" @click="setStatus(state.id)">
|
||||
<v-list-item-title>
|
||||
Set status to <v-icon small>{{ statusIcon(state.id) }}</v-icon> {{ state.name }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
·
|
||||
Kind:
|
||||
<v-menu offset-y class="mr-2">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<span v-bind="attrs" v-on="on">
|
||||
<v-icon small class="mr-1" :color="kindColor(artifact.kind)">{{ kindIcon(artifact.kind) }}</v-icon>
|
||||
<span :class="kindColor(artifact.kind) + '--text'">{{ artifact.kind | capitalize }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item dense link v-for="state in otherKinds" :key="state.id" @click="setKind(state.id)">
|
||||
<v-list-item-title>
|
||||
Set kind to <v-icon small>{{ kindIcon(state.id) }}</v-icon> {{ state.name }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
·
|
||||
Type:
|
||||
<v-menu
|
||||
:close-on-content-click="false"
|
||||
offset-y
|
||||
class="mr-2">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<span v-bind="attrs" v-on="on">
|
||||
{{ artifact.type ? artifact.type : "empty" }}
|
||||
</span>
|
||||
</template>
|
||||
<v-combobox
|
||||
class="pt-6 pb-0 px-3"
|
||||
style="background-color: white"
|
||||
v-model="artifacttype"
|
||||
:items="['ip', 'domain', 'md5', 'sha1', 'sha256', 'filename', 'url']"
|
||||
label="Artifact Type"
|
||||
@change="setArtifactType"
|
||||
></v-combobox>
|
||||
</v-menu>
|
||||
</h2>
|
||||
|
||||
<v-divider class="mt-0 mb-4"></v-divider>
|
||||
|
||||
<div v-if="automations">
|
||||
<v-card
|
||||
v-for="automation in artifactautomations" :key="automation.id" class="mb-4" elevation="0" outlined>
|
||||
<v-card-title>
|
||||
{{ automation.id }}
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
v-if="artifact && artifact.enrichments && automation.id in artifact.enrichments"
|
||||
@click="enrich(automation.id)"
|
||||
elevation="0"
|
||||
>
|
||||
<v-icon>mdi-sync</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-else @click="enrich(automation.id)" elevation="0">
|
||||
<v-icon>mdi-play</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text v-if="artifact && artifact.enrichments && automation.id in artifact.enrichments && artifact.enrichments[automation.id].data">
|
||||
<div class="template" v-if="artifact.enrichments[automation.id].html" v-html="artifact.enrichments[automation.id].html"></div>
|
||||
<JSONHTML v-else :json="artifact.enrichments[automation.id].data"></JSONHTML>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<div v-if="artifact">
|
||||
<div v-for="enrichment in artifact.enrichments" :key="enrichment.name" >
|
||||
<v-card v-if="!hasautomation(enrichment.name)" outlined>
|
||||
<v-card-title>
|
||||
{{ enrichment.name }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<vue-markdown>
|
||||
{{ enrichment.data }}
|
||||
</vue-markdown>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import {
|
||||
Artifact,
|
||||
AutomationResponse, AutomationResponseTypeEnum, AutomationTypeEnum,
|
||||
Type,
|
||||
TypeColorEnum
|
||||
} from "@/client";
|
||||
import VueMarkdown from "vue-markdown";
|
||||
import { API } from "@/services/api";
|
||||
import JSONHTML from "../components/JSONHTML.vue";
|
||||
|
||||
|
||||
interface State {
|
||||
artifact?: Artifact;
|
||||
automations?: Array<AutomationResponse>;
|
||||
artifacttype: string;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "ArtifactPopup",
|
||||
components: {
|
||||
"vue-markdown": VueMarkdown,
|
||||
JSONHTML,
|
||||
},
|
||||
props: ["id", "name"],
|
||||
data: (): State => ({
|
||||
artifact: undefined,
|
||||
automations: undefined,
|
||||
artifacttype: "unknown",
|
||||
}),
|
||||
watch: {
|
||||
id: function(): void {
|
||||
this.load();
|
||||
},
|
||||
name: function(): void {
|
||||
this.load();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
artifactautomations: function (): Array<AutomationResponse> {
|
||||
if (!this.automations) {
|
||||
return [];
|
||||
}
|
||||
return this.lodash.filter(this.automations, (automation: AutomationResponse) => {
|
||||
if (!automation || !automation.type) {
|
||||
return true;
|
||||
}
|
||||
return this.lodash.includes(automation.type, AutomationResponseTypeEnum.Artifact)
|
||||
})
|
||||
},
|
||||
ticketID(): number {
|
||||
return parseInt(this.id, 10);
|
||||
},
|
||||
otherStates: function (): Array<Type> {
|
||||
return this.lodash.filter(this.$store.state.settings.artifactStates, (state: Type) => {
|
||||
if (!this.artifact || !this.artifact.status) {
|
||||
return true;
|
||||
}
|
||||
return state.id !== this.artifact.status;
|
||||
})
|
||||
},
|
||||
otherKinds: function (): Array<Type> {
|
||||
return this.lodash.filter(this.$store.state.settings.artifactKinds, (state: Type) => {
|
||||
if (!this.artifact || !this.artifact.status) {
|
||||
return true;
|
||||
}
|
||||
return state.id !== this.artifact.status;
|
||||
})
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setArtifactType() {
|
||||
if (!this.artifact || !this.artifact.name || this.ticketID === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let artifact = this.artifact
|
||||
artifact.type = this.artifacttype
|
||||
API.setArtifact(this.ticketID, this.artifact.name, artifact).then((response) => {
|
||||
this.$store.dispatch("alertSuccess", { name: "Artifact type changed", type: "success" })
|
||||
if (response.data.artifacts) {
|
||||
this.lodash.forEach(response.data.artifacts, (artifact) => {
|
||||
if (artifact.name == this.name) {
|
||||
this.artifact = artifact;
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
hasautomation(name: string): boolean {
|
||||
let found = false;
|
||||
this.lodash.forEach(this.automations, (automation) => {
|
||||
if (automation.id === name) {
|
||||
found = true
|
||||
}
|
||||
})
|
||||
return found;
|
||||
},
|
||||
loadArtifact(id: number, artifact: string) {
|
||||
API.getArtifact(id, artifact).then((response) => {
|
||||
this.artifact = response.data;
|
||||
});
|
||||
},
|
||||
loadAutomations() {
|
||||
API.listAutomations().then((response) => {
|
||||
this.automations = response.data;
|
||||
});
|
||||
},
|
||||
enrich(automation: string) {
|
||||
if (this.artifact === undefined || this.ticketID === undefined) {
|
||||
return
|
||||
}
|
||||
API.runArtifact(this.ticketID, this.name, automation);
|
||||
},
|
||||
statusIcon: function (status: string): string {
|
||||
let icon = "mdi-help";
|
||||
this.lodash.forEach(this.$store.state.settings.artifactStates, (state: Type) => {
|
||||
if (status === state.id) {
|
||||
icon = state.icon;
|
||||
}
|
||||
})
|
||||
return icon;
|
||||
},
|
||||
statusColor: function (status: string) {
|
||||
let color = TypeColorEnum.Info as TypeColorEnum;
|
||||
this.lodash.forEach(this.$store.state.settings.artifactStates, (state: Type) => {
|
||||
if (status === state.id && state.color) {
|
||||
color = state.color
|
||||
}
|
||||
})
|
||||
return color;
|
||||
},
|
||||
setStatus(status: string) {
|
||||
if (!this.artifact || !this.artifact.name || this.ticketID === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let artifact = this.artifact
|
||||
artifact.status = status
|
||||
API.setArtifact(this.ticketID, this.artifact.name, artifact).then((response) => {
|
||||
this.$store.dispatch("alertSuccess", { name: "Artifact status changed", type: "success" })
|
||||
if (response.data.artifacts) {
|
||||
this.lodash.forEach(response.data.artifacts, (artifact) => {
|
||||
if (artifact.name == this.name) {
|
||||
this.artifact = artifact;
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
kindIcon: function (kind: string): string {
|
||||
let icon = "mdi-help";
|
||||
this.lodash.forEach(this.$store.state.settings.artifactKinds, (state: Type) => {
|
||||
if (kind === state.id) {
|
||||
icon = state.icon;
|
||||
}
|
||||
})
|
||||
return icon;
|
||||
},
|
||||
kindColor: function (kind: string) {
|
||||
let color = TypeColorEnum.Info as TypeColorEnum;
|
||||
this.lodash.forEach(this.$store.state.settings.artifactKinds, (state: Type) => {
|
||||
if (kind === state.id && state.color) {
|
||||
color = state.color
|
||||
}
|
||||
})
|
||||
return color;
|
||||
},
|
||||
setKind(kind: string) {
|
||||
if (!this.artifact || !this.artifact.name || this.ticketID === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let artifact = this.artifact
|
||||
artifact.kind = kind
|
||||
API.setArtifact(this.ticketID, this.artifact.name, artifact).then((response) => {
|
||||
this.$store.dispatch("alertSuccess", { name: "Artifact kind changed", type: "success" })
|
||||
if (response.data.artifacts) {
|
||||
this.lodash.forEach(response.data.artifacts, (artifact) => {
|
||||
if (artifact.name == this.name) {
|
||||
this.artifact = artifact;
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
load() {
|
||||
this.loadArtifact(this.ticketID, this.name);
|
||||
}
|
||||
},
|
||||
mounted(): void {
|
||||
this.load();
|
||||
this.loadAutomations();
|
||||
|
||||
this.$store.subscribeAction((action, state) => {
|
||||
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
|
||||
return
|
||||
}
|
||||
Vue.lodash.forEach(action.payload["ids"], (id) => {
|
||||
if (id === "tickets/" + this.ticketID) {
|
||||
this.load();
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.template {
|
||||
overflow: auto;
|
||||
}
|
||||
.template table, .template th, .template td {
|
||||
border: 1px solid #777 !important;
|
||||
border-left: 0 !important;
|
||||
border-right: 0 !important;
|
||||
padding: 4px !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,138 +0,0 @@
|
||||
<template>
|
||||
<div v-if="automation !== undefined" class="flex-grow-1 flex-column d-flex fill-height pa-8">
|
||||
<v-row class="flex-grow-0 flex-shrink-0">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn href="https://catalyst-soar.com/docs/catalyst/engineer/automations" target="_blank" outlined rounded small>
|
||||
<v-icon>mdi-book-open</v-icon> Handbook
|
||||
</v-btn>
|
||||
</v-row>
|
||||
|
||||
<v-alert v-if="readonly" type="info">You do not have write access to automations.</v-alert>
|
||||
<h2 v-if="readonly">Automation: {{ automation.id }}</h2>
|
||||
<h2 v-else-if="this.$route.params.id === 'new'">New Automation: {{ automation.id }}</h2>
|
||||
<h2 v-else>Edit Automation: {{ automation.id }}</h2>
|
||||
|
||||
<v-row class="flex-grow-0 flex-shrink-0">
|
||||
<v-col cols="12">
|
||||
<v-text-field :readonly="readonly" label="ID" v-model="automation.id" class="flex-grow-0 flex-shrink-0"></v-text-field>
|
||||
|
||||
<v-select
|
||||
label="Type"
|
||||
:items="types"
|
||||
item-text="id"
|
||||
return-object
|
||||
multiple
|
||||
v-model="automation.type"
|
||||
></v-select>
|
||||
|
||||
<v-text-field :readonly="readonly" label="Docker Image" v-model="automation.image" class="flex-grow-0 flex-shrink-0"></v-text-field>
|
||||
|
||||
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">Script</v-subheader>
|
||||
<div class="flex-grow-1 flex-shrink-1 overflow-scroll">
|
||||
<Editor v-model="automation.script" lang="python" :readonly="readonly" ></Editor>
|
||||
</div>
|
||||
|
||||
<AdvancedJSONSchemaEditor
|
||||
@save="save"
|
||||
:schema="automation.schema ? JSON.parse(automation.schema) : {}"
|
||||
hidepreview
|
||||
class="mt-4"
|
||||
:readonly="readonly"></AdvancedJSONSchemaEditor>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import Editor from "../components/Editor.vue";
|
||||
import AdvancedJSONSchemaEditor from "../components/AdvancedJSONSchemaEditor.vue";
|
||||
import {API} from "@/services/api";
|
||||
import {AutomationResponse, AutomationForm, AutomationResponseTypeEnum} from "@/client";
|
||||
import {DateTime} from "luxon";
|
||||
|
||||
interface State {
|
||||
automation?: AutomationResponse;
|
||||
data?: any;
|
||||
valid: boolean;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "Automation",
|
||||
components: { Editor, AdvancedJSONSchemaEditor },
|
||||
data: (): State => ({
|
||||
automation: undefined,
|
||||
data: undefined,
|
||||
valid: true,
|
||||
}),
|
||||
watch: {
|
||||
$route: function () {
|
||||
this.loadAutomation();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
readonly: function (): boolean {
|
||||
return !this.hasRole("automation:write");
|
||||
},
|
||||
types: function (): Array<string> {
|
||||
return [ AutomationResponseTypeEnum.Global, AutomationResponseTypeEnum.Playbook, AutomationResponseTypeEnum.Artifact ]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save(schema) {
|
||||
if (this.automation === undefined) {
|
||||
return;
|
||||
}
|
||||
let automation = this.automation as any as AutomationForm;
|
||||
automation.schema = schema;
|
||||
if (this.$route.params.id == "new") {
|
||||
API.createAutomation(automation);
|
||||
} else {
|
||||
API.updateAutomation(this.$route.params.id, automation);
|
||||
}
|
||||
},
|
||||
loadAutomation() {
|
||||
if (!this.$route.params.id) {
|
||||
return;
|
||||
}
|
||||
if (this.$route.params.id == "new") {
|
||||
this.automation = { id: "my-automation", image: "docker.io/ubuntu", script: "", type: [ AutomationResponseTypeEnum.Global, AutomationResponseTypeEnum.Playbook, AutomationResponseTypeEnum.Artifact ] };
|
||||
} else {
|
||||
API.getAutomation(this.$route.params.id).then((response) => {
|
||||
this.automation = response.data;
|
||||
});
|
||||
}
|
||||
},
|
||||
hasRole: function (s: string): boolean {
|
||||
if (this.$store.state.settings.roles) {
|
||||
return this.lodash.includes(this.$store.state.settings.roles, s);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
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);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadAutomation();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,64 +0,0 @@
|
||||
<template>
|
||||
<v-main style="min-height: 100vh">
|
||||
<List
|
||||
:items="automations"
|
||||
routername="Automation"
|
||||
itemid="id"
|
||||
itemname="id"
|
||||
singular="Automation"
|
||||
plural="Automations"
|
||||
writepermission="automation:write"
|
||||
@delete="deleteAutomation"
|
||||
></List>
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
import {AutomationResponse} from "@/client";
|
||||
import {API} from "@/services/api";
|
||||
import List from "../components/List.vue";
|
||||
|
||||
interface State {
|
||||
automations: Array<AutomationResponse>;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "AutomationList",
|
||||
components: {List},
|
||||
data: (): State => ({
|
||||
automations: [],
|
||||
}),
|
||||
methods: {
|
||||
loadAutomations() {
|
||||
API.listAutomations().then((response) => {
|
||||
this.automations = response.data;
|
||||
});
|
||||
},
|
||||
deleteAutomation(name: string) {
|
||||
API.deleteAutomation(name).then(() => {
|
||||
this.loadAutomations();
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadAutomations();
|
||||
|
||||
this.$store.subscribeAction((action, state) => {
|
||||
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
|
||||
return
|
||||
}
|
||||
let reload = false;
|
||||
Vue.lodash.forEach(action.payload["ids"], (id) => {
|
||||
if (this.lodash.startsWith(id, "automations/")) {
|
||||
reload = true;
|
||||
}
|
||||
});
|
||||
if (reload) {
|
||||
this.loadAutomations()
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,254 +0,0 @@
|
||||
<template>
|
||||
<div v-if="dashboard">
|
||||
<h2 class="d-flex">
|
||||
<span v-if="!editmode">{{ dashboard.name }}</span>
|
||||
<v-text-field v-else v-model="dashboard.name" outlined dense class="mb-0" hide-details></v-text-field>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn v-if="editmode" small outlined @click="addWidget" class="mr-1">
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
Add Widget
|
||||
</v-btn>
|
||||
<v-btn v-if="editmode" small outlined @click="save" class="mr-1">
|
||||
<v-icon>mdi-content-save</v-icon>
|
||||
Save
|
||||
</v-btn>
|
||||
<v-btn v-if="editmode && $route.params.id !== 'new'" small outlined @click="cancel">
|
||||
<v-icon>mdi-cancel</v-icon>
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn v-if="!editmode" small outlined @click="edit">
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
Edit
|
||||
</v-btn>
|
||||
</h2>
|
||||
|
||||
<v-row>
|
||||
<v-col v-for="(widget, index) in dashboard.widgets" :key="index" :cols="widget.width">
|
||||
<v-card class="mb-2">
|
||||
<v-card-title>
|
||||
<span v-if="!editmode">{{ widget.name }}</span>
|
||||
<v-text-field v-else outlined dense hide-details v-model="widget.name" class="mr-1"></v-text-field>
|
||||
<v-btn v-if="editmode" outlined @click="removeWidget(index)">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
Remove
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text v-if="editmode">
|
||||
<v-row>
|
||||
<v-col cols="8">
|
||||
<v-select label="Type" v-model="widget.type" :items="['line', 'bar', 'pie']"></v-select>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<v-text-field label="Width" type="number" v-model="widget.width"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-text-field label="Aggregation" v-model="widget.aggregation"></v-text-field>
|
||||
<v-text-field label="Filter" v-model="widget.filter" clearable></v-text-field>
|
||||
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text v-if="data[index] === null">
|
||||
{{ widgetErrors[index] }}
|
||||
</v-card-text>
|
||||
<div v-else>
|
||||
<line-chart
|
||||
v-if="widget.type === 'line' && data[index]"
|
||||
:chart-data="data[index]"
|
||||
:styles="{ width: '100%', position: 'relative' }"
|
||||
:chart-options="{
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
legend: false,
|
||||
scales: { yAxes: [ { ticks: { beginAtZero: true, precision: 0 } } ] }
|
||||
}"
|
||||
>
|
||||
</line-chart>
|
||||
|
||||
<pie-chart
|
||||
v-if="widget.type === 'pie' && data[index]"
|
||||
:chart-data="data[index]"
|
||||
:styles="{ width: '100%', position: 'relative' }"
|
||||
:chart-options="{
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
}"
|
||||
>
|
||||
</pie-chart>
|
||||
|
||||
<bar-chart
|
||||
v-if="widget.type === 'bar' && data[index]"
|
||||
:chart-data="data[index]"
|
||||
:styles="{
|
||||
width: '100%',
|
||||
'max-height': '400px',
|
||||
position: 'relative'
|
||||
}"
|
||||
:chart-options="{
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
legend: false,
|
||||
scales: { xAxes: [ { ticks: { beginAtZero: true, precision: 0 } } ] },
|
||||
}"
|
||||
></bar-chart>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import {DashboardResponse, Widget} from "@/client";
|
||||
import { API } from "@/services/api";
|
||||
import {createHash} from "crypto";
|
||||
import {colors} from "@/plugins/vuetify";
|
||||
import LineChart from "../components/charts/Line";
|
||||
import BarChart from "../components/charts/Bar";
|
||||
import PieChart from "../components/charts/Doughnut";
|
||||
import {ChartData} from "chart.js";
|
||||
import {AxiosError, AxiosResponseTransformer} from "axios";
|
||||
|
||||
interface State {
|
||||
dashboard?: DashboardResponse;
|
||||
undodashboard?: DashboardResponse;
|
||||
data: Record<string, any>;
|
||||
editmode: boolean;
|
||||
widgetErrors: Record<number, string>;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "Dashboard",
|
||||
components: {
|
||||
LineChart,
|
||||
BarChart,
|
||||
PieChart,
|
||||
},
|
||||
data: (): State => ({
|
||||
dashboard: undefined,
|
||||
undodashboard: undefined,
|
||||
data: {},
|
||||
editmode: false,
|
||||
widgetErrors: {},
|
||||
}),
|
||||
watch: {
|
||||
$route: function () {
|
||||
this.loadDashboard();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
edit: function () {
|
||||
this.undodashboard = this.lodash.cloneDeep(this.dashboard);
|
||||
this.editmode = true;
|
||||
},
|
||||
save: function () {
|
||||
if (!this.dashboard) {
|
||||
return
|
||||
}
|
||||
|
||||
let widgets = [] as Array<Widget>;
|
||||
this.lodash.forEach(this.dashboard.widgets, (widget) => {
|
||||
widget.width = this.lodash.toInteger(widget.width);
|
||||
if (!widget.filter) {
|
||||
this.lodash.unset(widget, "filter")
|
||||
}
|
||||
widgets.push(widget);
|
||||
})
|
||||
this.dashboard.widgets = widgets;
|
||||
|
||||
if (this.$route.params.id === 'new') {
|
||||
API.createDashboard(this.dashboard).then((response) => {
|
||||
this.loadWidgetData(response.data.widgets);
|
||||
|
||||
this.dashboard = response.data;
|
||||
this.editmode = false;
|
||||
|
||||
this.$router.push({ name: "Dashboard", params: { id: response.data.id }})
|
||||
})
|
||||
} else {
|
||||
API.updateDashboard(this.dashboard.id, this.dashboard).then((response) => {
|
||||
this.loadWidgetData(response.data.widgets);
|
||||
|
||||
this.dashboard = response.data;
|
||||
this.editmode = false;
|
||||
})
|
||||
}
|
||||
},
|
||||
cancel: function () {
|
||||
this.dashboard = this.lodash.cloneDeep(this.undodashboard);
|
||||
this.editmode = false;
|
||||
},
|
||||
addWidget: function () {
|
||||
if (!this.dashboard) {
|
||||
return
|
||||
}
|
||||
|
||||
this.dashboard.widgets.push({name: "new widget", width: 6, aggregation: "", type: "line"})
|
||||
},
|
||||
removeWidget: function (id: number) {
|
||||
if (!this.dashboard) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log(id);
|
||||
let widgets = this.lodash.cloneDeep(this.dashboard.widgets);
|
||||
this.lodash.pullAt(widgets, [id]);
|
||||
Vue.set(this.dashboard, "widgets", widgets);
|
||||
},
|
||||
loadDashboard: function () {
|
||||
if (this.$route.params.id === 'new') {
|
||||
this.dashboard = {
|
||||
name: "New dashboard",
|
||||
widgets: [{name: "new widget", width: 6, aggregation: "", type: "line"}],
|
||||
} as DashboardResponse
|
||||
this.editmode = true;
|
||||
} else {
|
||||
API.getDashboard(this.$route.params.id).then((response) => {
|
||||
this.loadWidgetData(response.data.widgets);
|
||||
|
||||
this.dashboard = response.data;
|
||||
});
|
||||
}
|
||||
},
|
||||
loadWidgetData: function (widgets: Array<Widget>) {
|
||||
this.lodash.forEach(widgets, (widget: Widget, index: number) => {
|
||||
let widgetErrors = {};
|
||||
let defaultTransformers = this.axios.defaults.transformResponse as AxiosResponseTransformer[]
|
||||
let transformResponse = defaultTransformers.concat((data) => {
|
||||
data.notoast = true;
|
||||
return data
|
||||
});
|
||||
API.dashboardData(widget.aggregation, widget.filter, {transformResponse: transformResponse}).then((response) => {
|
||||
let d = { labels: [], datasets: [{data: [], backgroundColor: []}] } as ChartData;
|
||||
this.lodash.forEach(response.data, (v: any, k: string) => {
|
||||
// @ts-expect-error T2532
|
||||
d.labels.push(k)
|
||||
// @ts-expect-error T2532
|
||||
d.datasets[0].data.push(v)
|
||||
|
||||
if (widget.type !== 'line') {
|
||||
// @ts-expect-error T2532
|
||||
d.datasets[0].backgroundColor.push(this.color(this.lodash.toString(v)));
|
||||
}
|
||||
})
|
||||
|
||||
Vue.set(this.data, index, d);
|
||||
}).catch((err: AxiosError) => {
|
||||
widgetErrors[index] = this.lodash.toString(err.response?.data.error);
|
||||
Vue.set(this.data, index, null);
|
||||
})
|
||||
Vue.set(this, 'widgetErrors', widgetErrors);
|
||||
})
|
||||
},
|
||||
color: function (s: string): string {
|
||||
let pos = createHash('md5').update(s).digest().readUInt32BE(0) % colors.length;
|
||||
return colors[pos];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadDashboard();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,64 +0,0 @@
|
||||
<template>
|
||||
<v-main style="min-height: 100vh;">
|
||||
<List
|
||||
:items="dashboards"
|
||||
routername="Dashboard"
|
||||
itemid="id"
|
||||
itemname="name"
|
||||
singular="Dashboard"
|
||||
plural="Dashboards"
|
||||
writepermission="dashboard:write"
|
||||
@delete="deleteDashboard"
|
||||
></List>
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
import {Dashboard} from "@/client";
|
||||
import {API} from "@/services/api";
|
||||
import List from "../components/List.vue";
|
||||
|
||||
interface State {
|
||||
dashboards: Array<Dashboard>;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "DashboardList",
|
||||
components: {List},
|
||||
data: (): State => ({
|
||||
dashboards: [],
|
||||
}),
|
||||
methods: {
|
||||
loadDashboards() {
|
||||
API.listDashboards().then((response) => {
|
||||
this.dashboards = response.data;
|
||||
});
|
||||
},
|
||||
deleteDashboard(id: string) {
|
||||
API.deleteDashboard(id).then(() => {
|
||||
this.loadDashboards();
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadDashboards();
|
||||
|
||||
this.$store.subscribeAction((action, state) => {
|
||||
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
|
||||
return
|
||||
}
|
||||
let reload = false;
|
||||
Vue.lodash.forEach(action.payload["ids"], (id) => {
|
||||
if (this.lodash.startsWith(id, "dashboard/")) {
|
||||
reload = true;
|
||||
}
|
||||
});
|
||||
if (reload) {
|
||||
this.loadDashboards()
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,141 @@
|
||||
<script setup lang="ts">
|
||||
import OpenTasks from '@/components/dashboard/OpenTasks.vue'
|
||||
import OpenTickets from '@/components/dashboard/OpenTickets.vue'
|
||||
import TicketOverTime from '@/components/dashboard/TicketOverTime.vue'
|
||||
import TicketTypes from '@/components/dashboard/TicketTypes.vue'
|
||||
import TwoColumn from '@/components/layout/TwoColumn.vue'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
|
||||
import { ExternalLink } from 'lucide-vue-next'
|
||||
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { pb } from '@/lib/pocketbase'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
isPending,
|
||||
isError,
|
||||
data: dashboardCounts,
|
||||
error
|
||||
} = useQuery({
|
||||
queryKey: ['dashboard_counts'],
|
||||
queryFn: (): Promise<Array<any>> => pb.collection('dashboard_counts').getFullList()
|
||||
})
|
||||
|
||||
const count = (id: string) => {
|
||||
if (!dashboardCounts.value) return 0
|
||||
|
||||
const s = dashboardCounts.value.filter((stat) => stat.id === id)
|
||||
if (s.length === 0) return 0
|
||||
|
||||
return s[0].count
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!pb.authStore.model) {
|
||||
router.push({ name: 'login' })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TwoColumn>
|
||||
<div class="flex h-screen flex-1 flex-col">
|
||||
<div class="flex h-14 items-center bg-background px-4 py-2">
|
||||
<h1 class="text-xl font-bold">Dashboard</h1>
|
||||
</div>
|
||||
<Separator class="shrink-0" />
|
||||
<ScrollArea>
|
||||
<div
|
||||
class="m-auto grid max-w-7xl grid-cols-1 grid-rows-[100px_100px_100px_100px] gap-4 p-4 md:grid-cols-2 md:grid-rows-[100px_100px] xl:grid-cols-4 xl:grid-rows-[100px]"
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ count('tasks') }}</CardTitle>
|
||||
<CardDescription>Tasks</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ count('tickets') }}</CardTitle>
|
||||
<CardDescription>Tickets</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ count('users') }}</CardTitle>
|
||||
<CardDescription>Users</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle></CardTitle>
|
||||
<CardDescription></CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle> Catalyst</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="flex flex-1 flex-col gap-1">
|
||||
<a
|
||||
href="https://catalyst-soar.com/docs/category/catalyst-handbook"
|
||||
target="_blank"
|
||||
class="flex items-center rounded border p-2 text-blue-500 hover:bg-accent"
|
||||
>
|
||||
Open Catalyst Handbook
|
||||
<ExternalLink class="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
<a
|
||||
href="/_/"
|
||||
target="_blank"
|
||||
class="flex items-center rounded border p-2 text-blue-500 hover:bg-accent"
|
||||
>
|
||||
Open Admin Interface
|
||||
<ExternalLink class="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle> Tickets by Type</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<TicketTypes />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card class="xl:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Tickets Per Week</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<TicketOverTime />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card class="xl:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Your Open Tickets</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<OpenTickets />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card class="xl:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Your Open Tasks</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<OpenTasks />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</TwoColumn>
|
||||
</template>
|
||||
@@ -1,217 +0,0 @@
|
||||
<template>
|
||||
<div class="network-graph fill-height">
|
||||
<v-card
|
||||
v-if="selected !== undefined"
|
||||
class="mt-3 ml-3 px-0"
|
||||
style="position: absolute; width: 33%; left: 60px; top: 60px; z-index: 5">
|
||||
<v-card-title>
|
||||
{{ selected.name }}
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
text
|
||||
:to="{ name: 'Graph', params: { col: col(selected.id), 'id': id(selected.id) } }"
|
||||
>
|
||||
<v-icon>mdi-bullseye</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
vv-if="selected.id.startsWith('artifacts/')"
|
||||
text
|
||||
@click="to(selected.id)"
|
||||
>
|
||||
<v-icon>mdi-open-in-new</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text class="ma-0 pa-0">
|
||||
<!--TicketSnippet v-if="selected.id.startsWith('tickets/')"></TicketSnippet-->
|
||||
<!--ArtifactSnippet v-if="selected.id.startsWith('artifacts/')"></ArtifactSnippet-->
|
||||
<IDSnippet :id="selected.id"></IDSnippet>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-main class="fill-height overflow-hidden">
|
||||
<div class="d-flex flex-column fill-height overflow-hidden">
|
||||
<v-row class="flex-grow-0 pb-4">
|
||||
<v-col cols="4" class="pl-8">
|
||||
<v-slider
|
||||
class="mt-4 mb-4"
|
||||
v-model="depth"
|
||||
dense
|
||||
:label="'Depth ' + depth"
|
||||
hide-details
|
||||
max="10"
|
||||
min="0"
|
||||
></v-slider>
|
||||
</v-col>
|
||||
<v-col cols="4" class="pl-4">
|
||||
<v-slider
|
||||
class="mt-4 mb-4"
|
||||
v-model="force"
|
||||
dense
|
||||
label="Node Distance"
|
||||
hide-details
|
||||
max="10000"
|
||||
min="0"
|
||||
></v-slider>
|
||||
</v-col>
|
||||
<v-col cols="2">
|
||||
<v-switch label="Hide Labels"
|
||||
v-model="hidelabel" class="mt-6 mb-4" hide-details>
|
||||
</v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="flex-grow-1 mt-0">
|
||||
<d3-network
|
||||
v-if="g !== undefined && g.links !== undefined"
|
||||
:net-nodes="nodes"
|
||||
:net-links="g.links"
|
||||
:options="options"
|
||||
@node-click="nodeclick"
|
||||
class="pt-0 mt-n4"
|
||||
style="width: 100%; height: 100%"
|
||||
/>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
import D3Network from "vue-d3-network";
|
||||
import {API} from "../services/api";
|
||||
import {Graph, Node} from "../client";
|
||||
// import TicketSnippet from "../components/snippets/TicketSnippet.vue";
|
||||
// import ArtifactSnippet from "../components/snippets/ArtifactSnippet.vue";
|
||||
import IDSnippet from "../components/snippets/IDSnippet.vue";
|
||||
|
||||
interface State {
|
||||
g?: Graph;
|
||||
force: number;
|
||||
depth: number;
|
||||
hidelabel: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
selected?: any;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "Graph",
|
||||
components: {
|
||||
IDSnippet,
|
||||
D3Network
|
||||
},
|
||||
data: (): State => ({
|
||||
g: undefined,
|
||||
force: 3000,
|
||||
depth: 2,
|
||||
hidelabel: false,
|
||||
selected: undefined,
|
||||
}),
|
||||
computed: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
options: function (): any {
|
||||
return {
|
||||
canvas: false,
|
||||
nodeLabels: !this.hidelabel,
|
||||
nodeSize: 20,
|
||||
linkWidth: 2,
|
||||
fontSize: 16,
|
||||
force: this.force
|
||||
}
|
||||
},
|
||||
nodes: function (): Array<any> {
|
||||
if (this.g === undefined || this.g.nodes === undefined) {
|
||||
return []
|
||||
}
|
||||
return this.lodash.map(this.g.nodes, (node: Node) => {
|
||||
if (node.id === this.$route.params.col + "/" + this.$route.params.id) {
|
||||
return {id: node.id, name: node.name, _size: 40, _cssClass: "center", _labelClass: "center"}
|
||||
}
|
||||
if (node.id.startsWith("tickets/")) {
|
||||
return {id: node.id, name: node.name, _size: 30, _cssClass: "ticket", _labelClass: "event"}
|
||||
}
|
||||
return {id: node.id, name: node.name}
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
depth: function () {
|
||||
this.fetchGraph();
|
||||
},
|
||||
$route: function () {
|
||||
this.fetchGraph();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
col: function (id: string): string {
|
||||
let parts = id.split("/");
|
||||
return parts[0];
|
||||
},
|
||||
id: function (id: string): string {
|
||||
let parts = id.split("/");
|
||||
return parts[1];
|
||||
},
|
||||
to: function (fid: string) {
|
||||
let col = this.col(fid);
|
||||
let id = this.id(fid);
|
||||
|
||||
if (col === 'tickets') {
|
||||
this.$router.push({
|
||||
name: "Ticket",
|
||||
params: { id: id, type: "-" }
|
||||
});
|
||||
return
|
||||
}
|
||||
|
||||
this.$router.push({
|
||||
name: "Artifact",
|
||||
params: { artifact: id }
|
||||
});
|
||||
},
|
||||
fetchGraph: function(): void {
|
||||
API.graph(this.$route.params.col, this.$route.params.id, this.depth).then((response) => {
|
||||
this.g = response.data;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
nodeclick(e: any, node: any) {
|
||||
this.selected = node;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchGraph();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.network-graph .node,
|
||||
.network-graph .node.selected {
|
||||
stroke: #388E3C !important;
|
||||
}
|
||||
.network-graph .node.event {
|
||||
stroke: #D32F2F !important;
|
||||
}
|
||||
.network-graph .node.center {
|
||||
stroke: #FFEB3B !important;
|
||||
fill: #FFEB3B !important;
|
||||
}
|
||||
|
||||
.theme--dark .network-graph .node-label,
|
||||
.theme--dark .network-graph .node-label.event {
|
||||
fill: #ffffff !important;
|
||||
}
|
||||
|
||||
.network-graph .node-label,
|
||||
.network-graph .node-label.event {
|
||||
fill: #000000 !important;
|
||||
}
|
||||
|
||||
.network-graph .link {
|
||||
stroke: #424242 !important;
|
||||
}
|
||||
.network-graph .link.selected,
|
||||
.network-graph .link:hover,.node:hover{
|
||||
stroke: #FFEB3B !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Group"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,62 +0,0 @@
|
||||
<template>
|
||||
<v-main style="min-height: 100vh;">
|
||||
<List
|
||||
:items="groups"
|
||||
routername="Group"
|
||||
itemid="id"
|
||||
itemname="name"
|
||||
singular="Group"
|
||||
plural="Groups"
|
||||
></List>
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
import {Group} from "../client";
|
||||
import {API} from "../services/api";
|
||||
import List from "../components/List.vue";
|
||||
|
||||
interface State {
|
||||
groups: Array<Group>;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "GroupList",
|
||||
components: {List},
|
||||
data: (): State => ({
|
||||
groups: [],
|
||||
}),
|
||||
methods: {
|
||||
loadGroups() {
|
||||
API.listGroups().then((response) => {
|
||||
this.groups = response.data;
|
||||
});
|
||||
},
|
||||
deleteGroup(name: string) {
|
||||
// API.deleteU(name).then(() => {
|
||||
// this.loadAutomations();
|
||||
// });
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadGroups();
|
||||
|
||||
this.$store.subscribeAction((action, state) => {
|
||||
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
|
||||
return
|
||||
}
|
||||
let reload = false;
|
||||
Vue.lodash.forEach(action.payload["ids"], (id) => {
|
||||
if (this.lodash.startsWith(id, "groups/")) {
|
||||
reload = true;
|
||||
}
|
||||
});
|
||||
if (reload) {
|
||||
this.loadGroups()
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,219 +0,0 @@
|
||||
<template>
|
||||
<v-main>
|
||||
<v-row>
|
||||
<v-col v-if="statistics" cols="12" lg="7">
|
||||
<v-row>
|
||||
<v-col cols="4">
|
||||
<v-subheader>Unassigned tickets</v-subheader>
|
||||
<span style="font-size: 60pt; text-align: center; display: block">
|
||||
<router-link :to="{
|
||||
name: 'TicketList',
|
||||
params: { query: 'status == \'open\' AND !owner' }
|
||||
}">
|
||||
{{ statistics.unassigned }}
|
||||
</router-link>
|
||||
</span>
|
||||
<v-subheader>Your tickets</v-subheader>
|
||||
<span style="font-size: 60pt; text-align: center; display: block">
|
||||
<router-link :to="{
|
||||
name: 'TicketList',
|
||||
params: { query: 'status == \'open\' AND owner == \'' + $store.state.user.id + '\'' }
|
||||
}">
|
||||
{{ $store.state.user.id in statistics.open_tickets_per_user ? statistics.open_tickets_per_user[$store.state.user.id] : 0 }}
|
||||
</router-link>
|
||||
</span>
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-subheader>Open tickets per owner</v-subheader>
|
||||
<bar-chart
|
||||
v-if="open_tickets_per_user"
|
||||
:chart-data="open_tickets_per_user"
|
||||
:styles="{
|
||||
width: '100%',
|
||||
'max-height': '400px',
|
||||
position: 'relative'
|
||||
}"
|
||||
:chart-options="{
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
legend: undefined,
|
||||
scales: { xAxes: [ { ticks: { beginAtZero: true, precision: 0 } } ] },
|
||||
onClick: clickUser,
|
||||
hover: {
|
||||
onHover: function(e) {
|
||||
var point = this.getElementAtEvent(e);
|
||||
if (point.length) e.target.style.cursor = 'pointer';
|
||||
else e.target.style.cursor = 'default';
|
||||
}
|
||||
}
|
||||
}"
|
||||
></bar-chart>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="7">
|
||||
<v-subheader>Tickets created per week</v-subheader>
|
||||
<line-chart
|
||||
v-if="tickets_per_week"
|
||||
:chart-data="tickets_per_week"
|
||||
:styles="{ width: '100%', position: 'relative' }"
|
||||
:chart-options="{
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
legend: undefined,
|
||||
scales: { yAxes: [ { ticks: { beginAtZero: true, precision: 0 } } ] }
|
||||
}"
|
||||
>
|
||||
</line-chart>
|
||||
</v-col>
|
||||
<v-col cols="5">
|
||||
<v-subheader>Ticket Types</v-subheader>
|
||||
<pie-chart
|
||||
v-if="tickets_per_type"
|
||||
:chart-data="tickets_per_type"
|
||||
:styles="{ width: '100%', position: 'relative' }"
|
||||
:chart-options="{
|
||||
onClick: clickPie,
|
||||
hover: {
|
||||
onHover: function(e) {
|
||||
var point = this.getElementAtEvent(e);
|
||||
if (point.length) e.target.style.cursor = 'pointer';
|
||||
else e.target.style.cursor = 'default';
|
||||
}
|
||||
}
|
||||
}"
|
||||
>
|
||||
</pie-chart>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
<v-col cols="12" lg="5">
|
||||
<TicketList :type="this.$route.params.type" @click="open"></TicketList>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import LineChart from "../components/charts/Line";
|
||||
import BarChart from "../components/charts/Bar";
|
||||
import PieChart from "../components/charts/Doughnut";
|
||||
import { API } from "@/services/api";
|
||||
import {Statistics, TicketResponse} from "@/client";
|
||||
import {DateTime} from "luxon";
|
||||
import { colors } from "@/plugins/vuetify";
|
||||
import TicketList from "@/components/TicketList.vue";
|
||||
import { createHash } from "crypto";
|
||||
|
||||
export default Vue.extend({
|
||||
name: "Home",
|
||||
components: {
|
||||
LineChart,
|
||||
BarChart,
|
||||
PieChart,
|
||||
TicketList
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
statistics: (undefined as unknown) as Statistics
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
tickets_per_type: function () {
|
||||
let data = { labels: [] as Array<string>, datasets: [{ backgroundColor: [] as Array<string>, data: [] as Array<number> }] }
|
||||
this.lodash.forEach(this.statistics.tickets_per_type, (count, type) => {
|
||||
data.labels.push(type);
|
||||
data.datasets[0].data.push(count);
|
||||
|
||||
data.datasets[0].backgroundColor.push(this.color(type));
|
||||
})
|
||||
return data
|
||||
},
|
||||
open_tickets_per_user: function () {
|
||||
let data = { labels: [] as Array<string>, datasets: [{ backgroundColor: [] as Array<string>, data: [] as Array<number> }] }
|
||||
this.lodash.forEach(this.statistics.open_tickets_per_user, (count, user) => {
|
||||
if (!user) {
|
||||
data.labels.push("unassigned");
|
||||
} else {
|
||||
data.labels.push(user);
|
||||
}
|
||||
data.datasets[0].data.push(count);
|
||||
data.datasets[0].backgroundColor.push(this.color(user));
|
||||
})
|
||||
return data
|
||||
},
|
||||
tickets_per_week: function () {
|
||||
let data = {labels: [] as Array<string>, datasets: [{backgroundColor: [] as Array<string>, data: [] as Array<number> }]}
|
||||
this.lodash.forEach(this.weeks(), (week) => {
|
||||
data.labels.push(week);
|
||||
if (week in this.statistics.tickets_per_week) {
|
||||
data.datasets[0].data.push(this.statistics.tickets_per_week[week]);
|
||||
} else {
|
||||
data.datasets[0].data.push(0);
|
||||
}
|
||||
data.datasets[0].backgroundColor.push("#607d8b");
|
||||
})
|
||||
return data
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open: function (ticket: TicketResponse) {
|
||||
if (ticket.id === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$router.push({
|
||||
name: "Ticket",
|
||||
params: {type: '-', id: ticket.id.toString()}
|
||||
});
|
||||
},
|
||||
clickUser: function (evt, elem) {
|
||||
let owner = this.open_tickets_per_user.labels[elem[0]._index];
|
||||
let query = 'status == \'open\' AND owner == \'' + owner + '\'';
|
||||
|
||||
if (owner == 'unassigned') {
|
||||
query = 'status == \'open\' AND !owner';
|
||||
}
|
||||
|
||||
this.$router.push({
|
||||
name: "TicketList",
|
||||
params: {query: query}
|
||||
});
|
||||
},
|
||||
clickPie: function (evt, elem) {
|
||||
this.$router.push({
|
||||
name: "TicketList",
|
||||
params: {type: this.tickets_per_type.labels[elem[0]._index]}
|
||||
});
|
||||
},
|
||||
color: function (s: string): string {
|
||||
let pos = createHash('md5').update(s).digest().readUInt32BE(0) % colors.length;
|
||||
return colors[pos];
|
||||
},
|
||||
fillData() {
|
||||
API.getStatistics().then(response => {
|
||||
this.statistics = response.data;
|
||||
});
|
||||
},
|
||||
weeks: function () {
|
||||
let w = [] as Array<string>;
|
||||
for (let i = 0; i < 53; i++) {
|
||||
w.push(DateTime.utc().minus({ weeks: i }).toFormat("kkkk-WW"))
|
||||
}
|
||||
this.lodash.reverse(w);
|
||||
return w
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fillData();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
canvas {
|
||||
position: relative !important;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,186 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="$route.params.id == 'new'" class="fill-height d-flex flex-column pa-8">
|
||||
<v-alert v-if="readonly" type="info">
|
||||
You do not have write access to jobs.
|
||||
</v-alert>
|
||||
<h2>Start new job</h2>
|
||||
|
||||
<v-select
|
||||
label="Automations"
|
||||
:items="globalautomations"
|
||||
item-text="id"
|
||||
return-object
|
||||
v-model="automation"
|
||||
></v-select>
|
||||
|
||||
<v-form v-if="automation" v-model="valid">
|
||||
<v-jsf
|
||||
v-model="data"
|
||||
:schema="JSON.parse(automation.schema)"
|
||||
:options="{ readonly: true, formats: { time: timeformat, date: dateformat, 'date-time': datetimeformat } }"
|
||||
/>
|
||||
<v-btn @click="run" color="success" outlined :disabled="!valid">
|
||||
Run
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</div>
|
||||
<div v-else-if="job !== undefined">
|
||||
<h2>Job: {{ job.id }}</h2>
|
||||
|
||||
<div v-if="job.automation" class="flex-grow-0 flex-shrink-0">
|
||||
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">Automation</v-subheader>
|
||||
{{ job.automation }}
|
||||
</div>
|
||||
|
||||
<div v-if="job.container" class="flex-grow-0 flex-shrink-0">
|
||||
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">Container</v-subheader>
|
||||
{{ job.container }}
|
||||
</div>
|
||||
|
||||
<div v-if="job.status" class="flex-grow-0 flex-shrink-0">
|
||||
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">Status</v-subheader>
|
||||
{{ job.status }}
|
||||
</div>
|
||||
|
||||
<div v-if="job.payload" class="flex-grow-0 flex-shrink-0">
|
||||
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">Input Payload</v-subheader>
|
||||
<Editor :value="JSON.stringify(job.payload, null, 2)" lang="json" :readonly="true"></Editor>
|
||||
</div>
|
||||
|
||||
<div v-if="job.log" class="flex-grow-0 flex-shrink-0">
|
||||
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">Log</v-subheader>
|
||||
<Editor :value="job.log" lang="log" :readonly="true"></Editor>
|
||||
</div>
|
||||
|
||||
<div v-if="job.output" class="flex-grow-0 flex-shrink-0">
|
||||
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">Output</v-subheader>
|
||||
<Editor :value="JSON.stringify(job.output, null, 2)" lang="json" :readonly="true"></Editor>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
import {AutomationResponse, AutomationResponseTypeEnum, JobResponse} from "@/client";
|
||||
import {API} from "@/services/api";
|
||||
import {DateTime} from "luxon";
|
||||
import Editor from "@/components/Editor.vue";
|
||||
|
||||
interface State {
|
||||
job?: JobResponse,
|
||||
data?: any;
|
||||
valid: boolean;
|
||||
|
||||
automation?: AutomationResponse;
|
||||
automations: Array<AutomationResponse>;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "Job",
|
||||
components: { Editor },
|
||||
data: (): State => ({
|
||||
job: undefined,
|
||||
data: undefined,
|
||||
valid: true,
|
||||
|
||||
automation: undefined,
|
||||
automations: [],
|
||||
}),
|
||||
watch: {
|
||||
$route: 'loadJob',
|
||||
},
|
||||
computed: {
|
||||
readonly: function (): boolean {
|
||||
return !this.hasRole("job:write");
|
||||
},
|
||||
globalautomations: function (): Array<AutomationResponse> {
|
||||
if (!this.automations) {
|
||||
return [];
|
||||
}
|
||||
return this.lodash.filter(this.automations, (automation: AutomationResponse) => {
|
||||
if (!automation || !automation.type) {
|
||||
return true;
|
||||
}
|
||||
return this.lodash.includes(automation.type, AutomationResponseTypeEnum.Global)
|
||||
})
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
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);
|
||||
},
|
||||
run: function () {
|
||||
if (!this.automation) {
|
||||
return;
|
||||
}
|
||||
API.runJob({ automation: this.automation.id, payload: this.data }).then(() => {
|
||||
this.$store.dispatch("alertSuccess", { name: "Job started." });
|
||||
})
|
||||
},
|
||||
loadJob() {
|
||||
if (!this.$route.params.id) {
|
||||
return
|
||||
}
|
||||
if (this.$route.params.id == 'new') {
|
||||
// this.template = { name: "MyTemplate", schema: '{ "type": "object", "name": "Incident" }' }
|
||||
// this.schema = { type: "object", name: "Incident" }
|
||||
} else {
|
||||
API.getJob(this.$route.params.id).then((response) => {
|
||||
this.job = response.data;
|
||||
});
|
||||
}
|
||||
},
|
||||
loadAutomations() {
|
||||
API.listAutomations().then((response) => {
|
||||
this.automations = response.data;
|
||||
});
|
||||
},
|
||||
hasRole: function (s: string): boolean {
|
||||
if (this.$store.state.settings.roles) {
|
||||
return this.lodash.includes(this.$store.state.settings.roles, s);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadJob();
|
||||
this.loadAutomations();
|
||||
|
||||
this.$store.subscribeAction((action, state) => {
|
||||
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
|
||||
return
|
||||
}
|
||||
let reload = false;
|
||||
Vue.lodash.forEach(action.payload["ids"], (id) => {
|
||||
if (this.lodash.startsWith(id, "jobs/")) {
|
||||
reload = true;
|
||||
}
|
||||
});
|
||||
if (reload) {
|
||||
this.loadJob()
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,108 +0,0 @@
|
||||
<template>
|
||||
<v-main style="min-height: 100vh;">
|
||||
<v-row class="fill-height ma-0">
|
||||
<v-col cols="3" class="listnav" style="">
|
||||
<v-list nav color="background">
|
||||
<v-list-item
|
||||
v-if="canWrite"
|
||||
:to="{ name: 'Job', 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 Job
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-subheader class="pl-4">Jobs</v-subheader>
|
||||
<v-list-item
|
||||
v-for="item in (jobs ? jobs : [])"
|
||||
:key="item.id"
|
||||
link
|
||||
:to="{ name: 'Job', params: { id: item.id } }"
|
||||
class="mx-2"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="item.status === 'created'">mdi-star</v-icon>
|
||||
<v-icon v-else-if="item.status === 'running'">mdi-run-fast</v-icon>
|
||||
<v-icon v-else-if="item.status === 'paused'">mdi-pause</v-icon>
|
||||
<v-icon v-else-if="item.status === 'restarting'">mdi-restart</v-icon>
|
||||
<v-icon v-else-if="item.status === 'removing'">mdi-close</v-icon>
|
||||
<v-icon v-else-if="item.status === 'exited'">mdi-exit-to-app</v-icon>
|
||||
<v-icon v-else-if="item.status === 'dead'">mdi-skull</v-icon>
|
||||
<v-icon v-else-if="item.status === 'completed'">mdi-check</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-subtitle>
|
||||
{{ item.id }}
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-title>
|
||||
{{ item.automation }} ({{ item.status }})
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-col>
|
||||
<v-col cols="9">
|
||||
<router-view></router-view>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
import {JobResponse} from "@/client";
|
||||
import {API} from "@/services/api";
|
||||
|
||||
interface State {
|
||||
jobs: Array<JobResponse>;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "JobList",
|
||||
components: {},
|
||||
data: (): State => ({
|
||||
jobs: [],
|
||||
}),
|
||||
computed: {
|
||||
canWrite: function (): boolean {
|
||||
return this.hasRole("job:write");
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
loadJobs() {
|
||||
API.listJobs().then((response) => {
|
||||
if (response.data) {
|
||||
this.jobs = response.data;
|
||||
}
|
||||
});
|
||||
},
|
||||
hasRole: function (s: string): boolean {
|
||||
if (this.$store.state.settings.roles) {
|
||||
return this.lodash.includes(this.$store.state.settings.roles, s);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadJobs();
|
||||
|
||||
this.$store.subscribeAction((action, state) => {
|
||||
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
|
||||
return
|
||||
}
|
||||
let reload = false;
|
||||
Vue.lodash.forEach(action.payload["ids"], (id) => {
|
||||
if (this.lodash.startsWith(id, "jobs/")) {
|
||||
reload = true;
|
||||
}
|
||||
});
|
||||
if (reload) {
|
||||
this.loadJobs()
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import { pb } from '@/lib/pocketbase'
|
||||
|
||||
const mail = ref('')
|
||||
const password = ref('')
|
||||
const errorMessage = ref('')
|
||||
|
||||
const login = () => {
|
||||
pb.collection('users')
|
||||
.authWithPassword(mail.value, password.value)
|
||||
.then(() => {
|
||||
window.location.href = '/ui/'
|
||||
})
|
||||
.catch((error) => {
|
||||
errorMessage.value = error.message
|
||||
})
|
||||
}
|
||||
|
||||
const { data: config } = useQuery({
|
||||
queryKey: ['config'],
|
||||
queryFn: (): Promise<Record<string, Array<String>>> => pb.send('/api/config', {})
|
||||
})
|
||||
|
||||
watch(config, (newConfig) => {
|
||||
if (!newConfig) return
|
||||
if (newConfig['flags'].includes('demo') || newConfig['flags'].includes('dev')) {
|
||||
mail.value = 'user@catalyst-soar.com'
|
||||
password.value = '1234567890'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full w-full flex-1 items-center justify-center">
|
||||
<Card class="m-auto w-96">
|
||||
<CardHeader class="flex flex-row justify-between">
|
||||
<CardTitle>Catalyst</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="flex flex-col gap-4">
|
||||
<Alert v-if="errorMessage" variant="destructive" class="border-4 p-4">
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{{ errorMessage }}</AlertDescription>
|
||||
</Alert>
|
||||
<Input
|
||||
v-model="mail"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
class="w-full"
|
||||
@keydown.enter="login"
|
||||
/>
|
||||
<Input
|
||||
v-model="password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
class="w-full"
|
||||
@keydown.enter="login"
|
||||
/>
|
||||
<Button variant="outline" class="w-full" @click="login"> Login</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<v-main>
|
||||
<div class="fill-height d-flex flex-row align-center justify-center">
|
||||
<h1>Page not found :(</h1>
|
||||
</div>
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "NotFound"
|
||||
}
|
||||
</script>
|
||||
@@ -1,293 +0,0 @@
|
||||
<template>
|
||||
<div v-if="playbook !== undefined" class="fill-height d-flex flex-column pa-8">
|
||||
<v-row class="flex-grow-0 flex-shrink-0">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn href="https://catalyst-soar.com/docs/catalyst/engineer/playbook" target="_blank" outlined rounded small>
|
||||
<v-icon>mdi-book-open</v-icon> Handbook
|
||||
</v-btn>
|
||||
</v-row>
|
||||
|
||||
<v-alert v-if="readonly" type="info">You do not have write access to playbooks.</v-alert>
|
||||
<h2 v-if="this.$route.params.id === 'new'">New Playbook</h2>
|
||||
<h2 v-else>Edit Playbook: {{ playbook.name }}</h2>
|
||||
|
||||
<v-alert v-if="formaterrors.length" color="warning">
|
||||
<div v-for="(formaterror, index) in formaterrors" :key="index">
|
||||
{{ formaterror.instancePath }}{{ formaterror.instancePath ? ": " : "" }}{{ formaterror.message }}
|
||||
</div>
|
||||
</v-alert>
|
||||
<v-alert v-else-if="error" color="warning">
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
|
||||
<div class="flex-grow-1 flex-shrink-1 overflow-scroll">
|
||||
<v-tabs
|
||||
v-model="tab"
|
||||
background-color="transparent"
|
||||
>
|
||||
<v-tab>Graph (experimental)</v-tab>
|
||||
<v-tab>YAML</v-tab>
|
||||
</v-tabs>
|
||||
<v-tabs-items v-model="tab" style="background: transparent">
|
||||
<v-tab-item>
|
||||
<v-text-field
|
||||
v-model="playbook.name"
|
||||
label="Name"
|
||||
outlined
|
||||
dense
|
||||
:readonly="readonly"
|
||||
class="mt-4"
|
||||
/>
|
||||
<PlaybookEditor
|
||||
v-if="playbookJSON"
|
||||
v-model="playbookJSON" />
|
||||
</v-tab-item>
|
||||
<v-tab-item>
|
||||
<v-card class="py-2">
|
||||
<Editor v-model="playbookYAML" lang="yaml" :readonly="readonly"></Editor>
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
</div>
|
||||
|
||||
<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 Vue from "vue";
|
||||
|
||||
import {Playbook, PlaybookTemplate, Task, TaskResponse} from "../client";
|
||||
import { API } from "@/services/api";
|
||||
import Editor from "../components/Editor.vue";
|
||||
import {alg, Graph} from "graphlib";
|
||||
import yaml from 'yaml';
|
||||
import Ajv from "ajv";
|
||||
import PlaybookEditor from "@/components/playbookeditor/PlaybookEditor.vue";
|
||||
|
||||
const playbookSchema = {
|
||||
type: "object",
|
||||
required: ["name", "tasks"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
tasks: {
|
||||
type: "object",
|
||||
additionalProperties: { $ref: "#/definitions/Task" }
|
||||
}
|
||||
},
|
||||
// additionalProperties: false,
|
||||
$id: "#/definitions/Playbook"
|
||||
};
|
||||
|
||||
const taskSchema = {
|
||||
type: "object",
|
||||
required: ["name", "type"],
|
||||
properties: {
|
||||
automation: { type: "string" },
|
||||
join: { type: "boolean" },
|
||||
payload: { type: "object", additionalProperties: { type: "string" } },
|
||||
name: { type: "string" },
|
||||
next: {
|
||||
type: "object",
|
||||
additionalProperties: { type: ["string", "null"] }
|
||||
},
|
||||
schema: { type: "object" },
|
||||
type: { type: "string", enum: ["task", "input", "automation"] }
|
||||
},
|
||||
// additionalProperties: false,
|
||||
$id: "#/definitions/Task"
|
||||
};
|
||||
|
||||
interface State {
|
||||
playbook?: PlaybookTemplate;
|
||||
g: Record<string, any>;
|
||||
selected: any;
|
||||
error: string;
|
||||
tab: number;
|
||||
playbookYAML: string;
|
||||
playbookJSON: any;
|
||||
}
|
||||
|
||||
interface TaskWithID {
|
||||
id: string;
|
||||
task: Task;
|
||||
}
|
||||
|
||||
const inityaml = "name: VirusTotal hash check\n" +
|
||||
"tasks:\n" +
|
||||
" input:\n" +
|
||||
" name: Please enter a word\n" +
|
||||
" type: input\n" +
|
||||
" schema:\n" +
|
||||
" title: Word\n" +
|
||||
" type: object\n" +
|
||||
" properties:\n" +
|
||||
" word:\n" +
|
||||
" type: string\n" +
|
||||
" title: Enter a Word\n" +
|
||||
" default: \"\"\n" +
|
||||
" next:\n" +
|
||||
" hash: \"word != ''\"\n" +
|
||||
"\n" +
|
||||
" hash:\n" +
|
||||
" name: Hash the word\n" +
|
||||
" type: automation\n" +
|
||||
" automation: hash.sha1\n" +
|
||||
" payload:\n" +
|
||||
" default: \"playbook.tasks['input'].data['word']\"\n" +
|
||||
" next:\n" +
|
||||
" end:\n" +
|
||||
"\n" +
|
||||
" end:\n" +
|
||||
" name: Finish the incident\n" +
|
||||
" type: task\n"
|
||||
|
||||
export default Vue.extend({
|
||||
name: "Playbook",
|
||||
components: { Editor, PlaybookEditor },
|
||||
data: (): State => ({
|
||||
playbook: undefined,
|
||||
g: {},
|
||||
selected: undefined,
|
||||
error: "",
|
||||
tab: 1,
|
||||
playbookJSON: undefined,
|
||||
playbookYAML: inityaml
|
||||
}),
|
||||
watch: {
|
||||
'$route': function () {
|
||||
this.loadPlaybook();
|
||||
},
|
||||
tab: function (value) {
|
||||
if (value === 0) {
|
||||
this.playbookJSON = yaml.parse(this.playbookYAML);
|
||||
} else {
|
||||
this.playbookYAML = yaml.stringify(this.playbookJSON);
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
formaterrors: function (): Array<any> {
|
||||
if (!this.playbook) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
let playbook = yaml.parse(this.playbook.yaml);
|
||||
const ajv = new Ajv({validateFormats: false});
|
||||
ajv.addSchema(taskSchema, "#/definitions/Task")
|
||||
const validate = ajv.compile(playbookSchema);
|
||||
const valid = validate(playbook)
|
||||
if (!valid && validate.errors) {
|
||||
return validate.errors;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
catch (e) {
|
||||
return [e];
|
||||
}
|
||||
},
|
||||
readonly: function (): boolean {
|
||||
return !this.hasRole("playbook:write");
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
gstatus: function(task: TaskResponse) {
|
||||
if (task.active) {
|
||||
return "open"
|
||||
}
|
||||
return "inactive"
|
||||
},
|
||||
tasks: function(g: any, playbook: Playbook): Array<TaskWithID> {
|
||||
let taskKeys = alg.topsort(g);
|
||||
let tasks = [] as Array<TaskWithID>;
|
||||
for (const tasksKey in taskKeys) {
|
||||
let taskWithID = {} as TaskWithID;
|
||||
if (playbook.tasks[taskKeys[tasksKey]] === undefined) {
|
||||
continue; // TODO
|
||||
}
|
||||
taskWithID.task = playbook.tasks[taskKeys[tasksKey]];
|
||||
taskWithID.id = taskKeys[tasksKey];
|
||||
tasks.push(taskWithID);
|
||||
}
|
||||
return tasks;
|
||||
},
|
||||
save() {
|
||||
if (this.playbook === undefined) {
|
||||
return;
|
||||
}
|
||||
let playbook = this.playbook;
|
||||
if (this.tab === 0) {
|
||||
let jsonData = this.playbookJSON;
|
||||
jsonData["name"] = playbook.name;
|
||||
playbook.yaml = yaml.stringify(jsonData);
|
||||
} else {
|
||||
playbook.yaml = this.playbookYAML;
|
||||
}
|
||||
|
||||
if (this.$route.params.id == 'new') {
|
||||
// playbook.id = kebabCase(playbook.name);
|
||||
API.createPlaybook(playbook).then(() => {
|
||||
this.$store.dispatch("alertSuccess", { name: "Playbook created" });
|
||||
});
|
||||
} else {
|
||||
API.updatePlaybook(this.$route.params.id, playbook).then(() => {
|
||||
this.$store.dispatch("alertSuccess", { name: "Playbook saved" });
|
||||
});
|
||||
}
|
||||
},
|
||||
loadPlaybook() {
|
||||
if (!this.$route.params.id) {
|
||||
return
|
||||
}
|
||||
if (this.$route.params.id == 'new') {
|
||||
this.playbook = { name: "MyPlaybook", yaml: inityaml }
|
||||
this.playbookJSON = yaml.parse(this.playbook.yaml);
|
||||
this.playbookYAML = this.playbook.yaml;
|
||||
} else {
|
||||
API.getPlaybook(this.$route.params.id).then((response) => {
|
||||
this.playbook = response.data;
|
||||
this.playbookJSON = yaml.parse(this.playbook.yaml);
|
||||
this.playbookYAML = this.playbook.yaml;
|
||||
});
|
||||
}
|
||||
},
|
||||
hasRole: function (s: string): boolean {
|
||||
if (this.$store.state.settings.roles) {
|
||||
return this.lodash.includes(this.$store.state.settings.roles, s);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadPlaybook();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.my-code {
|
||||
background: #2d2d2d;
|
||||
color: #ccc;
|
||||
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
|
||||
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.pipeline-node-label {
|
||||
fill: #333 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,66 +0,0 @@
|
||||
<template>
|
||||
<v-main style="min-height: 100vh;">
|
||||
<List
|
||||
:items="playbooks"
|
||||
routername="Playbook"
|
||||
itemid="id"
|
||||
itemname="name"
|
||||
singular="Playbook"
|
||||
plural="Playbooks"
|
||||
@delete="deletePlaybook"
|
||||
writepermission="playbook:write"
|
||||
></List>
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
import {PlaybookTemplate} from "../client";
|
||||
import {API} from "../services/api";
|
||||
import List from "../components/List.vue";
|
||||
|
||||
interface State {
|
||||
playbooks: Array<PlaybookTemplate>;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "PlaybookList",
|
||||
components: {List},
|
||||
data: (): State => ({
|
||||
playbooks: [],
|
||||
}),
|
||||
methods: {
|
||||
loadPlaybooks() {
|
||||
API.listPlaybooks().then((response) => {
|
||||
if (response.data) {
|
||||
this.playbooks = response.data;
|
||||
}
|
||||
});
|
||||
},
|
||||
deletePlaybook(name: string) {
|
||||
API.deletePlaybook(name).then(() => {
|
||||
this.loadPlaybooks();
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadPlaybooks();
|
||||
|
||||
this.$store.subscribeAction((action, state) => {
|
||||
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
|
||||
return
|
||||
}
|
||||
let reload = false;
|
||||
Vue.lodash.forEach(action.payload["ids"], (id) => {
|
||||
if (this.lodash.startsWith(id, "playbooks/")) {
|
||||
reload = true;
|
||||
}
|
||||
});
|
||||
if (reload) {
|
||||
this.loadPlaybooks()
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,53 +0,0 @@
|
||||
<template>
|
||||
<v-main v-if="userdata">
|
||||
<user-data-editor :userdata="userdata" @save="saveUserData"></user-data-editor>
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import { UserData } from "@/client";
|
||||
import { API } from "@/services/api";
|
||||
import UserDataEditor from "@/components/UserDataEditor.vue";
|
||||
import axios, {AxiosResponseTransformer} from "axios";
|
||||
|
||||
interface State {
|
||||
userdata?: UserData;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "Profile",
|
||||
data: (): State => ({
|
||||
userdata: undefined,
|
||||
}),
|
||||
components: {
|
||||
UserDataEditor,
|
||||
},
|
||||
watch: {
|
||||
$route: function () {
|
||||
this.loadUserData();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
saveUserData: function(userdata: UserData) {
|
||||
API.updateCurrentUserData(userdata).then(() => {
|
||||
this.$store.dispatch("alertSuccess", { name: "User data saved" });
|
||||
});
|
||||
},
|
||||
loadUserData: function () {
|
||||
const defaultTransformers = axios.defaults.transformResponse as AxiosResponseTransformer[]
|
||||
const transformResponse = defaultTransformers.concat((data) => {
|
||||
data.notoast = true;
|
||||
return data
|
||||
});
|
||||
API.currentUserData({transformResponse: transformResponse}).then((response) => {
|
||||
this.userdata = response.data;
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadUserData();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
<template>
|
||||
<div v-if="rule !== undefined" class="flex-grow-1 flex-column d-flex fill-height pa-8">
|
||||
<h2 v-if="this.$route.params.id === 'new'">New Rule: {{ rule.name }}</h2>
|
||||
<h2 v-else>Edit Rule: {{ rule.name }}</h2>
|
||||
|
||||
<v-text-field label="Name" v-model="rule.name" class="flex-grow-0 flex-shrink-0"></v-text-field>
|
||||
<v-text-field label="Condition" v-model="rule.condition" class="flex-grow-0 flex-shrink-0"></v-text-field>
|
||||
|
||||
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">
|
||||
Update
|
||||
</v-subheader>
|
||||
<div class="flex-grow-1 flex-shrink-1 overflow-scroll">
|
||||
<Editor v-model="update" lang="json"></Editor>
|
||||
</div>
|
||||
|
||||
<v-row 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" outlined @click="save">
|
||||
<v-icon>mdi-content-save</v-icon>
|
||||
Save
|
||||
</v-btn>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
import { Rule } from "@/client";
|
||||
import { API } from "@/services/api";
|
||||
import Editor from "../components/Editor.vue";
|
||||
|
||||
interface State {
|
||||
rule?: Rule;
|
||||
update: string;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "Rule",
|
||||
components: { Editor },
|
||||
data: (): State => ({
|
||||
rule: undefined,
|
||||
update: "",
|
||||
}),
|
||||
watch: {
|
||||
$route: function () {
|
||||
this.loadRule();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
if (this.rule === undefined) {
|
||||
return;
|
||||
}
|
||||
let rule = this.rule;
|
||||
// rule.id = kebabCase(rule.name);
|
||||
rule.update = JSON.parse(this.update);
|
||||
if (this.$route.params.id == 'new') {
|
||||
API.createRule(rule).then(() => {
|
||||
this.$store.dispatch("alertSuccess", { name: "Rule created" });
|
||||
});
|
||||
} else {
|
||||
API.updateRule(this.$route.params.id, rule).then(() => {
|
||||
this.$store.dispatch("alertSuccess", { name: "Rule saved" });
|
||||
});
|
||||
}
|
||||
},
|
||||
loadRule() {
|
||||
if (!this.$route.params.id) {
|
||||
return
|
||||
}
|
||||
if (this.$route.params.id == 'new') {
|
||||
this.rule = { name: "MyPlaybook", condition: "type == alert", update: { details: { status: "ignored" } } }
|
||||
} else {
|
||||
API.getRule(this.$route.params.id).then((response) => {
|
||||
this.rule = response.data;
|
||||
this.update = JSON.stringify(response.data.update, null, 4)
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadRule();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,63 +0,0 @@
|
||||
<template>
|
||||
<v-main style="min-height: 100vh;">
|
||||
<List
|
||||
:items="rules"
|
||||
routername="Rule"
|
||||
itemid="id"
|
||||
itemname="name"
|
||||
singular="Rule"
|
||||
plural="Rules"
|
||||
@delete="deleteRule"
|
||||
></List>
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
import {Rule} from "@/client";
|
||||
import {API} from "@/services/api";
|
||||
import List from "../components/List.vue";
|
||||
|
||||
interface State {
|
||||
rules: Array<Rule>;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "RuleList",
|
||||
components: {List},
|
||||
data: (): State => ({
|
||||
rules: [],
|
||||
}),
|
||||
methods: {
|
||||
loadRules() {
|
||||
API.listRules().then((response) => {
|
||||
this.rules = response.data;
|
||||
});
|
||||
},
|
||||
deleteRule(name: string) {
|
||||
API.deleteRule(name).then(() => {
|
||||
this.loadRules();
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadRules();
|
||||
|
||||
this.$store.subscribeAction((action, state) => {
|
||||
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
|
||||
return
|
||||
}
|
||||
let reload = false;
|
||||
Vue.lodash.forEach(action.payload["ids"], (id) => {
|
||||
if (this.lodash.startsWith(id, "rules/")) {
|
||||
reload = true;
|
||||
}
|
||||
});
|
||||
if (reload) {
|
||||
this.loadRules()
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,177 +0,0 @@
|
||||
<template>
|
||||
<v-main class="ma-4">
|
||||
<div v-if="settings !== undefined">
|
||||
<v-text-field label="Time Format" v-model="settings.timeformat"></v-text-field>
|
||||
|
||||
<v-subheader class="mx-0 px-0">Artifact States</v-subheader>
|
||||
<v-card v-for="state in settings.artifactStates" :key="state.id" class="d-flex mb-2">
|
||||
<v-row class="px-4 pt-2" dense>
|
||||
<v-col>
|
||||
<v-text-field label="ID" v-model="state.id"></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field label="Name" v-model="state.name"></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field label="Icon" v-model="state.icon"></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-select label="Color" v-model="state.color" :items="['info', 'error', 'success', 'warning']" clearable></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-btn icon class="mt-6 mr-4" @click="removeState(state.id)"><v-icon>mdi-close</v-icon></v-btn>
|
||||
</v-card>
|
||||
<v-card class="d-flex mb-2">
|
||||
<v-row class="px-4 pt-2" dense>
|
||||
<v-col>
|
||||
<v-text-field label="ID" v-model="newState.id"></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field label="Name" v-model="newState.name"></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field label="Icon" v-model="newState.icon"></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-select label="Color" v-model="newState.color" :items="['info', 'error', 'success', 'warning']" clearable></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-btn icon class="mt-6 mr-4" @click="addState"><v-icon>mdi-plus</v-icon></v-btn>
|
||||
</v-card>
|
||||
|
||||
<v-subheader class="mx-0 px-0">Artifact Types</v-subheader>
|
||||
<v-card v-for="state in settings.artifactKinds" :key="state.id" class="d-flex mb-2">
|
||||
<v-row class="px-4 pt-2" dense>
|
||||
<v-col>
|
||||
<v-text-field label="ID" v-model="state.id"></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field label="Name" v-model="state.name"></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field label="Icon" v-model="state.icon"></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-select label="Color" v-model="state.color" :items="['info', 'error', 'success', 'warning']" clearable></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-btn icon class="mt-6 mr-4" @click="removeKind(state.id)"><v-icon>mdi-close</v-icon></v-btn>
|
||||
</v-card>
|
||||
<v-card class="d-flex mb-2">
|
||||
<v-row class="px-4 pt-2" dense>
|
||||
<v-col>
|
||||
<v-text-field label="ID" v-model="newKind.id"></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field label="Name" v-model="newKind.name"></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field label="Icon" v-model="newKind.icon"></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-select label="Color" v-model="newKind.color" :items="['info', 'error', 'success', 'warning']" clearable></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-btn icon class="mt-6 mr-4" @click="addKind"><v-icon>mdi-plus</v-icon></v-btn>
|
||||
</v-card>
|
||||
|
||||
<v-btn color="success" @click="save" outlined class="mt-2">
|
||||
<v-icon>mdi-content-save</v-icon>
|
||||
Save
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {DateTime} from "luxon";
|
||||
import Vue from "vue";
|
||||
import {Settings, SettingsResponse, Type} from "@/client";
|
||||
import {API} from "@/services/api";
|
||||
import {AxiosResponse} from "axios";
|
||||
|
||||
interface State {
|
||||
valid: boolean;
|
||||
settings?: Settings;
|
||||
newState: Type;
|
||||
newKind: Type;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "Settings",
|
||||
data: (): State => ({
|
||||
valid: true,
|
||||
settings: undefined,
|
||||
newState: {} as Type,
|
||||
newKind: {} as Type,
|
||||
}),
|
||||
methods: {
|
||||
save: function () {
|
||||
if (this.settings === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
API.saveSettings(this.settings).then((response) => {
|
||||
this.settings = response.data;
|
||||
this.$store.dispatch("getSettings");
|
||||
})
|
||||
},
|
||||
addState: function () {
|
||||
if (this.settings === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
this.settings.artifactStates.push(this.newState);
|
||||
this.newState = {} as Type;
|
||||
},
|
||||
removeState: function (id: string) {
|
||||
if (this.settings === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
this.settings.artifactStates = this.lodash.filter(this.settings.artifactStates, function (t) { return t.id !== id });
|
||||
},
|
||||
addKind: function () {
|
||||
if (this.settings === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
this.settings.artifactKinds.push(this.newKind);
|
||||
this.newKind = {} as Type;
|
||||
},
|
||||
removeKind: function (id: string) {
|
||||
if (this.settings === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
this.settings.artifactKinds = this.lodash.filter(this.settings.artifactKinds, function (t) { return t.id !== id });
|
||||
},
|
||||
timeformat: function (s: 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) {
|
||||
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) {
|
||||
let format = this.$store.state.settings.timeformat;
|
||||
if (!format) {
|
||||
return DateTime.fromISO(s).toLocaleString(DateTime.DATETIME_SHORT);
|
||||
}
|
||||
return DateTime.fromISO(s).toFormat(format);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
API.getSettings().then((response: AxiosResponse<SettingsResponse>) => {
|
||||
this.settings = response.data;
|
||||
})
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,85 +0,0 @@
|
||||
<template>
|
||||
<v-main>
|
||||
<v-container>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="tasks ? tasks : []"
|
||||
item-key="name"
|
||||
multi-sort
|
||||
class="elevation-1 cards clickable"
|
||||
:loading="loading"
|
||||
:footer-props="{ 'items-per-page-options': [10, 25, 50, 100] }"
|
||||
@click:row="open"
|
||||
>
|
||||
</v-data-table>
|
||||
</v-container>
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import {TaskWithContext} from "@/client";
|
||||
import {API} from "@/services/api";
|
||||
|
||||
interface State {
|
||||
tasks: Array<TaskWithContext>;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "TaskList",
|
||||
data: (): State => ({
|
||||
tasks: [],
|
||||
loading: true,
|
||||
}),
|
||||
watch: {
|
||||
$route: function () {
|
||||
this.loadTasks();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
headers() {
|
||||
return [
|
||||
{
|
||||
text: "Ticket",
|
||||
align: "start",
|
||||
value: "ticket_name"
|
||||
},
|
||||
{
|
||||
text: "Playbook",
|
||||
align: "start",
|
||||
value: "playbook_name"
|
||||
},
|
||||
{
|
||||
text: "Task",
|
||||
align: "start",
|
||||
value: "task.name"
|
||||
},
|
||||
{
|
||||
text: "Owner",
|
||||
align: "start",
|
||||
value: "task.owner"
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(task: TaskWithContext) {
|
||||
this.$router.push({
|
||||
name: "Ticket",
|
||||
params: { id: task.ticket_id.toString(), type: "-" }
|
||||
});
|
||||
},
|
||||
loadTasks() {
|
||||
this.loading = true;
|
||||
API.listTasks().then((reponse) => {
|
||||
this.tasks = reponse.data;
|
||||
this.loading = false;
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadTasks();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,94 +0,0 @@
|
||||
<template>
|
||||
<div v-if="template !== undefined" class="flex-grow-1 flex-column d-flex fill-height pa-8">
|
||||
<v-row class="flex-grow-0 flex-shrink-0">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn href="https://catalyst-soar.com/docs/catalyst/engineer/template" target="_blank" outlined rounded small>
|
||||
<v-icon>mdi-book-open</v-icon> Handbook
|
||||
</v-btn>
|
||||
</v-row>
|
||||
|
||||
<v-alert v-if="readonly" type="info">
|
||||
You do not have write access to templates.
|
||||
Changes here cannot be saved.
|
||||
</v-alert>
|
||||
<div class="d-flex" style="align-items: center">
|
||||
<h2 v-if="readonly">Template: {{ template.name }}</h2>
|
||||
<h2 v-else-if="this.$route.params.id === 'new'">New Template: {{ template.name }}</h2>
|
||||
<h2 v-else>Edit Template: {{ template.name }}</h2>
|
||||
</div>
|
||||
|
||||
<v-text-field id="name-edit" label="Name" v-model="template.name" class="flex-grow-0 flex-shrink-0" :readonly="readonly"></v-text-field>
|
||||
|
||||
<AdvancedJSONSchemaEditor id="template-edit" v-if="schema" @save="save" :schema="schema" :readonly="readonly" :hidepreview="false"></AdvancedJSONSchemaEditor>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
import { TicketTemplate } from "@/client";
|
||||
import AdvancedJSONSchemaEditor from "../components/AdvancedJSONSchemaEditor.vue";
|
||||
|
||||
interface State {
|
||||
template?: TicketTemplate,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
schema?: any;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "Template",
|
||||
components: { AdvancedJSONSchemaEditor },
|
||||
data: (): State => ({
|
||||
template: undefined,
|
||||
schema: undefined,
|
||||
}),
|
||||
watch: {
|
||||
$route: 'loadTemplate',
|
||||
},
|
||||
computed: {
|
||||
readonly: function (): boolean {
|
||||
return !this.hasRole("template:write");
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
save(schema) {
|
||||
if (this.template === undefined) {
|
||||
return;
|
||||
}
|
||||
let template = this.template;
|
||||
template.schema = schema;
|
||||
|
||||
if (this.$route.params.id == 'new') {
|
||||
this.$store.dispatch("addTemplate", template).then(() => {
|
||||
this.$router.push({name: "TemplateList" });
|
||||
});
|
||||
} else {
|
||||
this.$store.dispatch("updateTemplate", { id: this.$route.params.id, template: template });
|
||||
}
|
||||
},
|
||||
loadTemplate() {
|
||||
if (!this.$route.params.id) {
|
||||
return
|
||||
}
|
||||
if (this.$route.params.id == 'new') {
|
||||
this.template = { name: "MyTemplate", schema: '{ "type": "object", "name": "Incident" }' }
|
||||
this.schema = { type: "object", name: "Incident" }
|
||||
} else {
|
||||
this.$store.dispatch("getTemplate", this.$route.params.id).then((response: TicketTemplate) => {
|
||||
this.template = response;
|
||||
this.schema = JSON.parse(response.schema);
|
||||
});
|
||||
}
|
||||
},
|
||||
hasRole: function (s: string): boolean {
|
||||
if (this.$store.state.settings.roles) {
|
||||
return this.lodash.includes(this.$store.state.settings.roles, s);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadTemplate();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<v-main style="min-height: 100vh;">
|
||||
<List
|
||||
:items="$store.state.templates.templates"
|
||||
routername="Template"
|
||||
itemid="id"
|
||||
itemname="name"
|
||||
singular="Template"
|
||||
plural="Templates"
|
||||
writepermission="template:write"
|
||||
@delete="deleteTemplate"
|
||||
></List>
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
import List from "../components/List.vue";
|
||||
|
||||
export default Vue.extend({
|
||||
name: "TemplateList",
|
||||
components: {List},
|
||||
methods: {
|
||||
deleteTemplate(title: string) {
|
||||
this.$store.dispatch("deleteTemplate", title).then(() => {
|
||||
this.$router.push({name: "TemplateList"});
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch("listTemplates");
|
||||
|
||||
this.$store.subscribeAction((action, state) => {
|
||||
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
|
||||
return
|
||||
}
|
||||
let reload = false;
|
||||
Vue.lodash.forEach(action.payload["ids"], (id) => {
|
||||
if (this.lodash.startsWith(id, "templates/")) {
|
||||
reload = true;
|
||||
}
|
||||
});
|
||||
if (reload) {
|
||||
this.$store.dispatch("listTemplates");
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,77 +0,0 @@
|
||||
<template>
|
||||
<v-main style="min-height: 100vh">
|
||||
<splitpanes class="default-theme" @resize="paneSize = $event[0].size">
|
||||
<pane class="pa-3" :size="paneSize">
|
||||
<TicketListComponent :type="$route.params.type" :query="query" @click="open" @new="opennew"></TicketListComponent>
|
||||
</pane>
|
||||
<pane v-if="this.$route.params.id" class="pa-3" :size="100 - paneSize">
|
||||
<v-row>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="close" outlined rounded class="mt-2 mr-2" small>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
Close
|
||||
</v-btn>
|
||||
</v-row>
|
||||
<router-view></router-view>
|
||||
</pane>
|
||||
</splitpanes>
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import {TicketResponse} from "@/client";
|
||||
import { Splitpanes, Pane } from 'splitpanes';
|
||||
import 'splitpanes/dist/splitpanes.css';
|
||||
|
||||
import TicketListComponent from "../components/TicketList.vue";
|
||||
|
||||
export default Vue.extend({
|
||||
name: "TicketList",
|
||||
components: {
|
||||
TicketListComponent,
|
||||
Splitpanes, Pane,
|
||||
},
|
||||
props: ['query', 'type'],
|
||||
data: () => ({
|
||||
paneSize: 30,
|
||||
}),
|
||||
methods: {
|
||||
numeric: function (n: any): boolean {
|
||||
return !isNaN(parseFloat(n)) && !isNaN(n - 0);
|
||||
},
|
||||
open: function (ticket: TicketResponse) {
|
||||
this.$router.push({
|
||||
name: "Ticket",
|
||||
params: {type: this.$route.params.type, id: ticket.id.toString()}
|
||||
}).then(() => {
|
||||
this.paneSize = 30;
|
||||
});
|
||||
},
|
||||
opennew: function () {
|
||||
this.paneSize = 30;
|
||||
},
|
||||
close: function () {
|
||||
this.$router.push({
|
||||
name: "TicketList",
|
||||
params: {type: this.$route.params.type},
|
||||
}).then(() => {
|
||||
this.paneSize = 100;
|
||||
});
|
||||
},
|
||||
hidelist: function () {
|
||||
this.paneSize = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.splitpanes.default-theme .splitpanes__pane {
|
||||
background: none;
|
||||
}
|
||||
.splitpanes.default-theme .splitpanes__splitter {
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,168 +0,0 @@
|
||||
<template>
|
||||
<div class="mt-8">
|
||||
<h2>New {{ $route.params.type | capitalize }}</h2>
|
||||
<v-form class="create clearfix">
|
||||
<v-text-field id="title-edit" label="Title" v-model="name"></v-text-field>
|
||||
|
||||
<div id="playbooks-edit">
|
||||
<v-select
|
||||
label="Playbooks"
|
||||
:items="playbooks"
|
||||
item-text="name"
|
||||
return-object
|
||||
multiple
|
||||
v-model="selectedPlaybooks"
|
||||
></v-select>
|
||||
</div>
|
||||
|
||||
<div id="templates-edit">
|
||||
<v-select
|
||||
label="Template"
|
||||
:items="templates"
|
||||
item-text="name"
|
||||
return-object
|
||||
v-model="selectedTemplate"
|
||||
></v-select>
|
||||
</div>
|
||||
|
||||
<v-subheader class="pl-0 mt-4" style="height: 20px">Details</v-subheader>
|
||||
<div v-if="selectedTemplate !== undefined" class="details">
|
||||
<v-lazy>
|
||||
<v-jsf
|
||||
v-model="details"
|
||||
:options="{ fieldProps: { 'hide-details': true } }"
|
||||
:schema="selectedSchema"
|
||||
/>
|
||||
</v-lazy>
|
||||
</div>
|
||||
|
||||
<v-btn color="green" @click="createTicket" outlined>
|
||||
Create
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
import {
|
||||
PlaybookTemplateForm,
|
||||
PlaybookTemplateResponse,
|
||||
TicketForm,
|
||||
TicketTemplateResponse,
|
||||
TicketType
|
||||
} from "@/client";
|
||||
import { API } from "@/services/api";
|
||||
|
||||
interface State {
|
||||
selectedTemplate?: TicketTemplateResponse;
|
||||
templates: Array<TicketTemplateResponse>;
|
||||
|
||||
selectedPlaybooks: Array<PlaybookTemplateResponse>;
|
||||
playbooks: Array<PlaybookTemplateResponse>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
details: any;
|
||||
name: string;
|
||||
ticketType?: TicketType;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "TicketNew",
|
||||
// components: { VJsf },
|
||||
data: (): State => ({
|
||||
selectedTemplate: undefined,
|
||||
templates: [],
|
||||
selectedPlaybooks: [],
|
||||
playbooks: [],
|
||||
name: "",
|
||||
details: {},
|
||||
ticketType: undefined,
|
||||
}),
|
||||
computed: {
|
||||
selectedSchema() {
|
||||
if (
|
||||
this.selectedTemplate === undefined ||
|
||||
this.selectedTemplate.schema === undefined
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
return JSON.parse(this.selectedTemplate.schema);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createTicket: function() {
|
||||
if (
|
||||
this.selectedTemplate === undefined ||
|
||||
this.selectedTemplate?.schema === undefined
|
||||
) {
|
||||
this.$store.dispatch("alertError", { name: "No template selected" });
|
||||
return;
|
||||
}
|
||||
|
||||
let ticket: TicketForm = {
|
||||
type: this.$route.params.type,
|
||||
name: this.name,
|
||||
details: this.details,
|
||||
schema: this.selectedTemplate?.schema,
|
||||
status: "open",
|
||||
owner: this.$store.state.user.id,
|
||||
playbooks: this.selectedPlaybooks as Array<PlaybookTemplateForm>,
|
||||
};
|
||||
|
||||
API.createTicket(ticket).then(response => {
|
||||
if (response.data.id === undefined) {
|
||||
return;
|
||||
}
|
||||
this.$router.push({
|
||||
name: "Ticket",
|
||||
params: {
|
||||
id: response.data.id.toString(),
|
||||
type: response.data.type,
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
API.getTicketType(this.$route.params.type).then((response) => {
|
||||
this.ticketType = response.data;
|
||||
|
||||
API.listTemplates().then(response => {
|
||||
this.templates = response.data;
|
||||
this.lodash.forEach(this.templates, (template) => {
|
||||
if (this.ticketType && template.id == this.ticketType.default_template) {
|
||||
this.selectedTemplate = template;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
API.listPlaybooks().then(response => {
|
||||
this.playbooks = response.data;
|
||||
this.lodash.forEach(this.playbooks, (playbook) => {
|
||||
if (this.ticketType && this.lodash.includes(this.ticketType.default_playbooks, playbook.id)) {
|
||||
this.selectedPlaybooks.push(playbook);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.details > .col > .row > .row,
|
||||
.details > .col > .row > div > .row {
|
||||
margin-top: 16px !important;
|
||||
padding: 16px !important;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.v-application .create .vjsf-property {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.v-application .create .error--text {
|
||||
color: white !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,126 +0,0 @@
|
||||
<template>
|
||||
<div v-if="tickettype !== undefined" class="flex-grow-1 flex-column d-flex fill-height pa-8">
|
||||
<h2 v-if="this.$route.params.id === 'new'">New Ticket Type: {{ tickettype.name }}</h2>
|
||||
<h2 v-else>Edit Ticket Type: {{ tickettype.name }}</h2>
|
||||
|
||||
<v-text-field label="Name" v-model="tickettype.name" class="flex-grow-0 flex-shrink-0"></v-text-field>
|
||||
|
||||
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">
|
||||
Icon (see <a href="https://materialdesignicons.com" class="mx-1"> materialdesignicons.com </a> and prefix with "mdi-")
|
||||
</v-subheader>
|
||||
<v-text-field v-model="tickettype.icon" placeholder="e.g. mdi-alert" class="flex-grow-0 flex-shrink-0 mt-n3"></v-text-field>
|
||||
|
||||
|
||||
<v-select
|
||||
v-model="selectedTemplate"
|
||||
:items="templates"
|
||||
item-text="name"
|
||||
label="Default Template"
|
||||
return-object
|
||||
class="flex-grow-0 flex-shrink-0"
|
||||
></v-select>
|
||||
|
||||
<v-select
|
||||
v-model="selectedPlaybooks"
|
||||
:items="playbooks"
|
||||
item-text="name"
|
||||
label="Default Playbooks"
|
||||
return-object
|
||||
multiple
|
||||
class="flex-grow-0 flex-shrink-0"
|
||||
></v-select>
|
||||
|
||||
<v-row 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" outlined @click="save">
|
||||
<v-icon>mdi-content-save</v-icon>
|
||||
Save
|
||||
</v-btn>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
import { PlaybookTemplateResponse, TicketTemplateResponse, TicketType } from "@/client";
|
||||
import { API } from "@/services/api";
|
||||
|
||||
interface State {
|
||||
tickettype?: TicketType;
|
||||
selectedPlaybooks: Array<PlaybookTemplateResponse>;
|
||||
playbooks?: Array<PlaybookTemplateResponse>;
|
||||
selectedTemplate?: TicketTemplateResponse;
|
||||
templates?: Array<TicketTemplateResponse>;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "TicketType",
|
||||
data: (): State => ({
|
||||
tickettype: undefined,
|
||||
selectedPlaybooks: [],
|
||||
playbooks: undefined,
|
||||
selectedTemplate: undefined,
|
||||
templates: undefined,
|
||||
}),
|
||||
watch: {
|
||||
$route: function () {
|
||||
this.loadRule();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
if (this.tickettype === undefined) {
|
||||
return;
|
||||
}
|
||||
let tickettype = this.tickettype;
|
||||
if (this.selectedTemplate) {
|
||||
tickettype.default_template = this.selectedTemplate.id;
|
||||
}
|
||||
tickettype.default_playbooks = [];
|
||||
this.lodash.forEach(this.selectedPlaybooks, (playbook) => {
|
||||
tickettype.default_playbooks.push(playbook.id)
|
||||
})
|
||||
|
||||
if (this.$route.params.id == 'new') {
|
||||
API.createTicketType(tickettype).then(() => {
|
||||
this.$store.dispatch("alertSuccess", { name: "Ticket type created" });
|
||||
});
|
||||
} else {
|
||||
API.updateTicketType(this.$route.params.id, tickettype).then(() => {
|
||||
this.$store.dispatch("alertSuccess", { name: "Ticket type saved" });
|
||||
});
|
||||
}
|
||||
},
|
||||
loadRule() {
|
||||
if (!this.$route.params.id) {
|
||||
return
|
||||
}
|
||||
|
||||
API.listTemplates().then((response) => {
|
||||
this.templates = response.data;
|
||||
if (response.data.length > 0) {
|
||||
this.selectedTemplate = response.data[0];
|
||||
}
|
||||
})
|
||||
API.listPlaybooks().then((response) => {
|
||||
this.playbooks = response.data;
|
||||
})
|
||||
|
||||
if (this.$route.params.id == 'new') {
|
||||
this.tickettype = { name: "TI Ticket", default_groups: [], default_playbooks: [], default_template: "", icon: "" }
|
||||
} else {
|
||||
API.getTicketType(this.$route.params.id).then((response) => {
|
||||
this.tickettype = response.data;
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadRule();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,64 +0,0 @@
|
||||
<template>
|
||||
<v-main style="min-height: 100vh;">
|
||||
<List
|
||||
:items="tickettypes"
|
||||
routername="TicketType"
|
||||
itemid="id"
|
||||
itemname="name"
|
||||
singular="Ticket Type"
|
||||
plural="Ticket Types"
|
||||
@delete="deleteTicketType"
|
||||
writepermission="tickettype:write"
|
||||
></List>
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
import { TicketType} from "@/client";
|
||||
import {API} from "@/services/api";
|
||||
import List from "../components/List.vue";
|
||||
|
||||
interface State {
|
||||
tickettypes: Array<TicketType>;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "TicketTypeList",
|
||||
components: {List},
|
||||
data: (): State => ({
|
||||
tickettypes: [],
|
||||
}),
|
||||
methods: {
|
||||
loadTicketType() {
|
||||
API.listTicketTypes().then((response) => {
|
||||
this.tickettypes = response.data;
|
||||
});
|
||||
},
|
||||
deleteTicketType(id: string) {
|
||||
API.deleteTicketType(id).then(() => {
|
||||
this.loadTicketType();
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadTicketType();
|
||||
|
||||
this.$store.subscribeAction((action, state) => {
|
||||
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
|
||||
return
|
||||
}
|
||||
let reload = false;
|
||||
Vue.lodash.forEach(action.payload["ids"], (id) => {
|
||||
if (this.lodash.startsWith(id, "tickettypes/")) {
|
||||
reload = true;
|
||||
}
|
||||
});
|
||||
if (reload) {
|
||||
this.loadTicketType()
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import TanView from '@/components/TanView.vue'
|
||||
import ThreeColumn from '@/components/layout/ThreeColumn.vue'
|
||||
import TicketDisplay from '@/components/ticket/TicketDisplay.vue'
|
||||
import TicketList from '@/components/ticket/TicketList.vue'
|
||||
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { pb } from '@/lib/pocketbase'
|
||||
import type { Type } from '@/lib/types'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const id = computed(() => route.params.id as string)
|
||||
const type = computed(() => route.params.type as string)
|
||||
|
||||
const {
|
||||
isPending,
|
||||
isError,
|
||||
data: selectedType,
|
||||
error,
|
||||
refetch
|
||||
} = useQuery({
|
||||
queryKey: ['types', type.value],
|
||||
queryFn: (): Promise<Type> => pb.collection('types').getOne(type.value)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => type.value,
|
||||
() => refetch()
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (!pb.authStore.model) {
|
||||
router.push({ name: 'login' })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ThreeColumn>
|
||||
<template #list>
|
||||
<TanView :isError="isError" :isPending="isPending" :error="error" :value="selectedType">
|
||||
<TicketList v-if="selectedType" :key="selectedType.id" :selectedType="selectedType" />
|
||||
</TanView>
|
||||
</template>
|
||||
<template #single>
|
||||
<TanView :isError="isError" :isPending="isPending" :error="error" :value="selectedType">
|
||||
<div
|
||||
v-if="!id"
|
||||
class="flex h-full w-full items-center justify-center text-lg text-gray-500"
|
||||
>
|
||||
No ticket selected
|
||||
</div>
|
||||
<TicketDisplay v-else-if="selectedType" :key="id" :selectedType="selectedType" />
|
||||
</TanView>
|
||||
</template>
|
||||
</ThreeColumn>
|
||||
</template>
|
||||
@@ -1,138 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="user === 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="fill-height d-flex flex-column pa-8">
|
||||
<div v-if="$route.params.id === 'new'">
|
||||
<h2>
|
||||
Create a new
|
||||
<span v-if="user.apikey">API Key</span>
|
||||
<span v-else>User</span>
|
||||
</h2>
|
||||
<i>Users can only be created via OIDC.</i>
|
||||
<v-form>
|
||||
<!--v-btn-toggle v-model="user.apikey" mandatory dense>
|
||||
<v-btn :value="false">User</v-btn>
|
||||
<v-btn :value="true">API Key</v-btn>
|
||||
</v-btn-toggle-->
|
||||
<v-text-field label="ID" v-model="user.id" class="mb-2" :rules="[
|
||||
v => !!v || 'ID is required',
|
||||
v => (v && v.length < 254) || 'ID must be between 1 and 254 characters',
|
||||
v => /^[a-z\d\-]+$/.test(v) || 'Only characters a-z, 0-9 and - are allowed',
|
||||
// v => /^[A-Za-z0-9_\-\:@\(\)\+,=;\$!\*'%]+$/.test(v) || 'Only characters A-Z, a-z, 0-9, _, -, :, ., @, (, ), +, ,, =, ;, $, !, *, \', % are allowed',
|
||||
]"></v-text-field>
|
||||
<v-text-field
|
||||
v-if="!user.apikey"
|
||||
label="Password"
|
||||
v-model="user.password"
|
||||
:append-icon="show ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
:type="show ? 'text' : 'password'"
|
||||
@click:append="show = !show"
|
||||
:rules="[
|
||||
v => !!v || 'Password is required',
|
||||
v => (v && v.length >= 8) || 'Password must be at least 8 characters',
|
||||
]"></v-text-field>
|
||||
<v-select multiple chips label="Roles" v-model="user.roles" :items="['analyst', 'engineer', 'admin']"></v-select>
|
||||
<v-btn @click="save" color="success" outlined>
|
||||
<v-icon>mdi-plus-thick</v-icon>
|
||||
Create
|
||||
<span v-if="user.apikey" class="ml-1">API Key</span>
|
||||
<span v-else class="ml-1">User</span>
|
||||
</v-btn>
|
||||
</v-form>
|
||||
<v-alert v-if="newUserResponse" color="warning" class="mt-4" dismissible>
|
||||
<b>New API secret:</b> {{ newUserResponse.secret }}<br>
|
||||
Make sure you save it - you won't be able to access it again.
|
||||
</v-alert>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h2>
|
||||
{{ user.id }}
|
||||
<span v-if="user.apikey">(API Key)</span>
|
||||
</h2>
|
||||
|
||||
<!--v-text-field v-if="!user.apikey" label="New Password (leave empty to keep)" v-model="user.password" hide-details class="mb-4"></v-text-field-->
|
||||
<v-checkbox v-if="!user.apikey" label="Blocked" v-model="user.blocked" hide-details class="mb-4"></v-checkbox>
|
||||
|
||||
<v-select multiple chips v-if="!user.apikey" label="Roles" v-model="user.roles" :items="['analyst', 'engineer', 'admin']"></v-select>
|
||||
<div v-else>
|
||||
<v-chip v-for="role in user.roles" :key="role" class="mr-1 mb-1">{{ role }}</v-chip>
|
||||
</div>
|
||||
|
||||
<v-btn v-if="!user.apikey" @click="save" color="success" outlined>
|
||||
<v-icon>mdi-content-save</v-icon>
|
||||
Save
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
import {NewUserResponse, UserForm} from "@/client";
|
||||
import {API} from "@/services/api";
|
||||
|
||||
interface State {
|
||||
show: boolean;
|
||||
user?: UserForm;
|
||||
newUserResponse?: NewUserResponse;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "User",
|
||||
components: {},
|
||||
data: (): State => ({
|
||||
show: false,
|
||||
user: undefined,
|
||||
newUserResponse: undefined
|
||||
}),
|
||||
watch: {
|
||||
'$route': function () {
|
||||
this.loadUser();
|
||||
this.newUserResponse = undefined;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
if (!this.user) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.$route.params.id == 'new') {
|
||||
API.createUser(this.user).then(response => {
|
||||
this.newUserResponse = response.data;
|
||||
})
|
||||
} else {
|
||||
API.updateUser(this.$route.params.id, this.user).then(response => {
|
||||
this.user = response.data;
|
||||
})
|
||||
}
|
||||
},
|
||||
loadUser() {
|
||||
if (!this.$route.params.id) {
|
||||
return
|
||||
}
|
||||
if (this.$route.params.id == 'new') {
|
||||
this.user = {id: "", roles: [], blocked: false, apikey: true}
|
||||
} else {
|
||||
API.getUser(this.$route.params.id).then((response) => {
|
||||
this.user = response.data;
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadUser();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<div v-if="userdata">
|
||||
<user-data-editor :userdata="userdata" @save="saveUserData"></user-data-editor>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import { UserData } from "@/client";
|
||||
import { API } from "@/services/api";
|
||||
import UserDataEditor from "@/components/UserDataEditor.vue";
|
||||
|
||||
interface State {
|
||||
userdata?: UserData;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "UserData",
|
||||
data: (): State => ({
|
||||
userdata: undefined,
|
||||
}),
|
||||
components: { UserDataEditor },
|
||||
watch: {
|
||||
$route: function () {
|
||||
this.loadUserData();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
saveUserData: function(userdata: UserData) {
|
||||
API.updateUserData(this.$route.params.id, userdata).then(() => {
|
||||
this.$store.dispatch("alertSuccess", { name: "User data saved" });
|
||||
});
|
||||
},
|
||||
loadUserData: function () {
|
||||
API.getUserData(this.$route.params.id).then((response) => {
|
||||
this.userdata = response.data;
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadUserData();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,60 +0,0 @@
|
||||
<template>
|
||||
<v-main style="min-height: 100vh;">
|
||||
<List
|
||||
:items="userdatas"
|
||||
routername="UserData"
|
||||
itemid="id"
|
||||
itemname="name"
|
||||
singular="User Data"
|
||||
plural="User Data"
|
||||
:show-new="false"
|
||||
:deletable="false"
|
||||
writepermission="userdata:write"
|
||||
></List>
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
import {UserData} from "@/client";
|
||||
import {API} from "@/services/api";
|
||||
import List from "../components/List.vue";
|
||||
|
||||
interface State {
|
||||
userdatas: Array<UserData>;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "UserDataList",
|
||||
components: {List},
|
||||
data: (): State => ({
|
||||
userdatas: [],
|
||||
}),
|
||||
methods: {
|
||||
loadSettings() {
|
||||
API.listUserData().then((response) => {
|
||||
this.userdatas = response.data;
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadSettings();
|
||||
|
||||
this.$store.subscribeAction((action, state) => {
|
||||
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
|
||||
return
|
||||
}
|
||||
let reload = false;
|
||||
Vue.lodash.forEach(action.payload["ids"], (id) => {
|
||||
if (this.lodash.startsWith(id, "settings/")) {
|
||||
reload = true;
|
||||
}
|
||||
});
|
||||
if (reload) {
|
||||
this.loadSettings()
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,66 +0,0 @@
|
||||
<template>
|
||||
<v-main style="min-height: 100vh;">
|
||||
<List
|
||||
:items="users"
|
||||
routername="User"
|
||||
itemid="id"
|
||||
itemname="id"
|
||||
singular="User / API Key"
|
||||
plural="Users / API Keys"
|
||||
@delete="deleteUser"
|
||||
writepermission="user:write"
|
||||
></List>
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
import {UserResponse} from "@/client";
|
||||
import {API} from "@/services/api";
|
||||
import List from "../components/List.vue";
|
||||
|
||||
interface State {
|
||||
users: Array<UserResponse>;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "UserList",
|
||||
components: {List},
|
||||
data: (): State => ({
|
||||
users: [],
|
||||
}),
|
||||
methods: {
|
||||
loadUsers() {
|
||||
API.listUsers().then((response) => {
|
||||
if (response.data) {
|
||||
this.users = response.data;
|
||||
}
|
||||
});
|
||||
},
|
||||
deleteUser(name: string) {
|
||||
API.deleteUser(name).then(() => {
|
||||
this.loadUsers();
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadUsers();
|
||||
|
||||
this.$store.subscribeAction((action, state) => {
|
||||
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
|
||||
return
|
||||
}
|
||||
let reload = false;
|
||||
Vue.lodash.forEach(action.payload["ids"], (id) => {
|
||||
if (this.lodash.startsWith(id, "users/")) {
|
||||
reload = true;
|
||||
}
|
||||
});
|
||||
if (reload) {
|
||||
this.loadUsers()
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<object data="http://localhost:8003">
|
||||
<embed src="http://localhost:8003" />
|
||||
Error: Embedded data could not be displayed.
|
||||
</object>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Arango",
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
object,
|
||||
embed {
|
||||
padding-left: 56px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<object data="http://localhost:9000/minio">
|
||||
<embed src="http://localhost:9000/minio" />
|
||||
Error: Embedded data could not be displayed.
|
||||
</object>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Minio",
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
object,
|
||||
embed {
|
||||
padding-left: 56px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<object data="http://localhost:1880">
|
||||
<embed src="http://localhost:1880" />
|
||||
Error: Embedded data could not be displayed.
|
||||
</object>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Nodered",
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
object,
|
||||
embed {
|
||||
padding-left: 56px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user