mirror of
https://github.com/SecurityBrewery/catalyst.git
synced 2026-06-13 03:39:12 +02:00
Release catalyst
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
<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>
|
||||
@@ -0,0 +1,264 @@
|
||||
<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>
|
||||
·
|
||||
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;
|
||||
})
|
||||
},
|
||||
},
|
||||
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;
|
||||
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;
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
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>
|
||||
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div v-if="automation !== undefined" class="flex-grow-1 flex-column d-flex fill-height pa-8">
|
||||
<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("engineer: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.user.roles) {
|
||||
return this.lodash.includes(this.$store.state.user.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>
|
||||
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<v-main style="min-height: 100vh">
|
||||
<List
|
||||
:items="automations"
|
||||
routername="Automation"
|
||||
itemid="id"
|
||||
itemname="id"
|
||||
singular="Automation"
|
||||
plural="Automations"
|
||||
writepermission="engineer: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>
|
||||
@@ -0,0 +1,219 @@
|
||||
<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: "Dashboard",
|
||||
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>
|
||||
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<div class="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>
|
||||
.node,
|
||||
.node.selected {
|
||||
stroke: #388E3C !important;
|
||||
}
|
||||
.node.event {
|
||||
stroke: #D32F2F !important;
|
||||
}
|
||||
.node.center {
|
||||
stroke: #FFEB3B !important;
|
||||
fill: #FFEB3B !important;
|
||||
}
|
||||
|
||||
.theme--dark .node-label,
|
||||
.theme--dark .node-label.event {
|
||||
fill: #ffffff !important;
|
||||
}
|
||||
|
||||
.node-label,
|
||||
.node-label.event {
|
||||
fill: #000000 !important;
|
||||
}
|
||||
|
||||
.link {
|
||||
stroke: #424242 !important;
|
||||
}
|
||||
.link.selected,
|
||||
.link:hover,.node:hover{
|
||||
stroke: #FFEB3B !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Group"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,62 @@
|
||||
<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>
|
||||
@@ -0,0 +1,186 @@
|
||||
<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("admin: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(this.$route.params.id).then((response) => {
|
||||
this.automations = response.data;
|
||||
});
|
||||
},
|
||||
hasRole: function (s: string): boolean {
|
||||
if (this.$store.state.user.roles) {
|
||||
return this.lodash.includes(this.$store.state.user.roles, s);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.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>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<v-main style="min-height: 100vh;">
|
||||
<List
|
||||
:items="jobs"
|
||||
routername="Job"
|
||||
itemid="id"
|
||||
itemname="id"
|
||||
singular="Job"
|
||||
plural="Jobs"
|
||||
writepermission="admin:job:write"
|
||||
:deletable="false"
|
||||
></List>
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
import {JobResponse} from "@/client";
|
||||
import {API} from "@/services/api";
|
||||
import List from "../components/List.vue";
|
||||
|
||||
interface State {
|
||||
jobs: Array<JobResponse>;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "JobList",
|
||||
components: {List},
|
||||
data: (): State => ({
|
||||
jobs: [],
|
||||
}),
|
||||
methods: {
|
||||
loadJobs() {
|
||||
API.listJobs().then((response) => {
|
||||
if (response.data) {
|
||||
this.jobs = response.data;
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
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,311 @@
|
||||
<template>
|
||||
<div v-if="playbook !== undefined" class="fill-height d-flex flex-column pa-8">
|
||||
<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 v-else class="px-4 overflow-scroll">
|
||||
<vue-pipeline
|
||||
v-if="pipelineData"
|
||||
ref="pipeline"
|
||||
:x="50"
|
||||
:y="55"
|
||||
:data="pipelineData"
|
||||
:showArrow="true"
|
||||
:ystep="70"
|
||||
:xstep="100"
|
||||
lineStyle="default"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">
|
||||
Playbook
|
||||
</v-subheader>
|
||||
<div class="flex-grow-1 flex-shrink-1 overflow-scroll">
|
||||
<Editor v-model="playbook.yaml" @input="updatePipeline" lang="yaml" :readonly="readonly"></Editor>
|
||||
</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";
|
||||
|
||||
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" },
|
||||
msg: { 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;
|
||||
pipelineData: any;
|
||||
error: string;
|
||||
}
|
||||
|
||||
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" +
|
||||
" msg:\n" +
|
||||
" payload: \"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 },
|
||||
data: (): State => ({
|
||||
playbook: undefined,
|
||||
g: {},
|
||||
selected: undefined,
|
||||
pipelineData: undefined,
|
||||
error: "",
|
||||
}),
|
||||
watch: {
|
||||
'$route': function () {
|
||||
this.loadPlaybook();
|
||||
}
|
||||
},
|
||||
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("engineer: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;
|
||||
},
|
||||
updatePipeline: function () {
|
||||
if (this.playbook) {
|
||||
this.pipeline(this.playbook.yaml);
|
||||
}
|
||||
},
|
||||
pipeline: function(playbookYAML: string) {
|
||||
try {
|
||||
let playbook = yaml.parse(playbookYAML);
|
||||
|
||||
this.error = "";
|
||||
|
||||
let g = new Graph();
|
||||
|
||||
for (const stepKey in playbook.tasks) {
|
||||
g.setNode(stepKey);
|
||||
}
|
||||
|
||||
this.lodash.forEach(playbook.tasks, (task: Task, stepKey: string) => {
|
||||
if ("next" in task) {
|
||||
this.lodash.forEach(task.next, (condition, nextKey) => {
|
||||
g.setEdge(stepKey, nextKey);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let tasks = this.tasks(g, playbook);
|
||||
let elements = [] as Array<any>;
|
||||
this.lodash.forEach(tasks, task => {
|
||||
elements.push({
|
||||
id: task.id,
|
||||
name: task.task.name,
|
||||
next: [],
|
||||
status: "unknown"
|
||||
});
|
||||
});
|
||||
|
||||
this.lodash.forEach(tasks, (task: TaskWithID) => {
|
||||
if ("next" in task.task) {
|
||||
this.lodash.forEach(task.task.next, (condition, nextKey) => {
|
||||
let nextID = this.lodash.findIndex(elements, ["id", nextKey]);
|
||||
let stepID = this.lodash.findIndex(elements, ["id", task.id]);
|
||||
if (nextID !== -1) {
|
||||
// TODO: invalid schema
|
||||
elements[stepID].next.push({index: nextID});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.pipelineData = undefined;
|
||||
this.$nextTick(() => {
|
||||
this.pipelineData = this.lodash.values(elements);
|
||||
})
|
||||
}
|
||||
catch (e: unknown) {
|
||||
console.log(e);
|
||||
this.error = this.lodash.toString(e);
|
||||
}
|
||||
},
|
||||
save() {
|
||||
if (this.playbook === undefined) {
|
||||
return;
|
||||
}
|
||||
if (this.$route.params.id == 'new') {
|
||||
let playbook = this.playbook;
|
||||
// playbook.id = kebabCase(playbook.name);
|
||||
API.createPlaybook(playbook).then(() => {
|
||||
this.$store.dispatch("alertSuccess", { name: "Playbook created" });
|
||||
});
|
||||
} else {
|
||||
API.updatePlaybook(this.$route.params.id, this.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 }
|
||||
} else {
|
||||
API.getPlaybook(this.$route.params.id).then((response) => {
|
||||
this.playbook = response.data;
|
||||
});
|
||||
}
|
||||
},
|
||||
hasRole: function (s: string): boolean {
|
||||
if (this.$store.state.user.roles) {
|
||||
return this.lodash.includes(this.$store.state.user.roles, s);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.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>
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<v-main style="min-height: 100vh;">
|
||||
<List
|
||||
:items="playbooks"
|
||||
routername="Playbook"
|
||||
itemid="id"
|
||||
itemname="name"
|
||||
singular="Playbook"
|
||||
plural="Playbooks"
|
||||
@delete="deletePlaybook"
|
||||
writepermission="engineer: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>
|
||||
@@ -0,0 +1,47 @@
|
||||
<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";
|
||||
|
||||
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 () {
|
||||
API.currentUserData().then((response) => {
|
||||
this.userdata = response.data;
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadUserData();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<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>
|
||||
@@ -0,0 +1,63 @@
|
||||
<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>
|
||||
@@ -0,0 +1,85 @@
|
||||
<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 {TaskResponse, TaskWithContext} from "@/client";
|
||||
import {API} from "@/services/api";
|
||||
|
||||
interface State {
|
||||
tasks: Array<TaskResponse>;
|
||||
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>
|
||||
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div v-if="template !== undefined" class="flex-grow-1 flex-column d-flex fill-height pa-8">
|
||||
<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 label="Name" v-model="template.name" class="flex-grow-0 flex-shrink-0" :readonly="readonly"></v-text-field>
|
||||
|
||||
<AdvancedJSONSchemaEditor v-if="schema" @save="save" :schema="schema" :readonly="readonly"></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("engineer: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.user.roles) {
|
||||
return this.lodash.includes(this.$store.state.user.roles, s);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadTemplate();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<v-main style="min-height: 100vh;">
|
||||
<List
|
||||
:items="$store.state.templates.templates"
|
||||
routername="Template"
|
||||
itemid="id"
|
||||
itemname="name"
|
||||
singular="Template"
|
||||
plural="Templates"
|
||||
writepermission="engineer: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
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<v-main>
|
||||
<TicketListComponent :type="this.$route.params.type" :query="query" @click="open"></TicketListComponent>
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import {TicketResponse} from "@/client";
|
||||
|
||||
import TicketListComponent from "../components/TicketList.vue";
|
||||
|
||||
export default Vue.extend({
|
||||
name: "TicketList",
|
||||
components: {TicketListComponent},
|
||||
props: ['query'],
|
||||
methods: {
|
||||
open: function (ticket: TicketResponse) {
|
||||
if (ticket.id === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$router.push({
|
||||
name: "Ticket",
|
||||
params: {type: this.$route.params.type, id: ticket.id.toString()}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<v-main>
|
||||
<v-container>
|
||||
<v-card color="cards">
|
||||
<v-card-title>New {{ $route.params.type | capitalize }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-form class="create clearfix">
|
||||
<v-text-field label="Title" v-model="name"></v-text-field>
|
||||
|
||||
<v-select
|
||||
label="Playbooks"
|
||||
:items="playbooks"
|
||||
item-text="name"
|
||||
return-object
|
||||
multiple
|
||||
v-model="selectedPlaybooks"
|
||||
></v-select>
|
||||
|
||||
<v-select
|
||||
label="Template"
|
||||
:items="templates"
|
||||
item-text="name"
|
||||
return-object
|
||||
v-model="selectedTemplate"
|
||||
></v-select>
|
||||
|
||||
<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>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</v-main>
|
||||
</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>
|
||||
@@ -0,0 +1,126 @@
|
||||
<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>
|
||||
@@ -0,0 +1,64 @@
|
||||
<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="engineer: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,105 @@
|
||||
<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 new API key</h2>
|
||||
<v-form>
|
||||
<v-text-field label="ID" v-model="user.id" hide-details></v-text-field>
|
||||
<v-select multiple chips label="Roles" v-model="user.roles" :items="$store.state.settings.roles"></v-select>
|
||||
<v-btn @click="save" color="success" outlined>
|
||||
<v-icon>mdi-plus-thick</v-icon>
|
||||
Create API-Key
|
||||
</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-select multiple chips v-if="!user.apikey" label="Roles" v-model="user.roles" :items="$store.state.settings.roles"></v-select>
|
||||
<div v-else>
|
||||
<v-chip v-for="role in user.roles" :key="role">{{ 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, UserResponse } from "../client";
|
||||
import {API} from "@/services/api";
|
||||
|
||||
interface State {
|
||||
user?: UserResponse;
|
||||
newUserResponse?: NewUserResponse;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: "User",
|
||||
components: {},
|
||||
data: (): State => ({
|
||||
user: undefined,
|
||||
newUserResponse: undefined
|
||||
}),
|
||||
watch: {
|
||||
'$route': function () {
|
||||
this.loadUser();
|
||||
}
|
||||
},
|
||||
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>
|
||||
@@ -0,0 +1,44 @@
|
||||
<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>
|
||||
@@ -0,0 +1,60 @@
|
||||
<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="admin: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>
|
||||
@@ -0,0 +1,66 @@
|
||||
<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="admin: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>
|
||||
@@ -0,0 +1,21 @@
|
||||
<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>
|
||||
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<object data="http://localhost:9001/keygen">
|
||||
<embed src="http://localhost:9001/keygen" />
|
||||
Error: Embedded data could not be displayed.
|
||||
</object>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Emitter",
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
object,
|
||||
embed {
|
||||
padding-left: 56px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,21 @@
|
||||
<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>
|
||||
@@ -0,0 +1,21 @@
|
||||
<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