+
-
-
+
+
{{ icon }}
{{ buttonText }}
@@ -18,7 +18,7 @@
export default {
name: "TooltipButtonToggle",
props: {
- value: {
+ modelValue: {
type: Boolean,
default: false,
},
@@ -35,13 +35,14 @@ export default {
required: true,
},
},
+ emits: ["update:modelValue"],
data() {
return {
internalValue: false,
};
},
watch: {
- value: {
+ modelValue: {
immediate: true,
handler(newValue) {
this.internalValue = newValue;
@@ -49,7 +50,7 @@ export default {
},
internalValue: {
handler(newValue) {
- this.$emit("input", newValue);
+ this.$emit("update:modelValue", !!newValue);
},
},
},
diff --git a/src/components/agents/AgentDownloadDialog.vue b/src/components/agents/AgentDownloadDialog.vue
index 7d6d15fb..e0603f16 100644
--- a/src/components/agents/AgentDownloadDialog.vue
+++ b/src/components/agents/AgentDownloadDialog.vue
@@ -13,8 +13,8 @@
v-model="pathToFile"
label="path/to/file"
:rules="rules['pathToFile']"
- outlined
- dense
+ variant="outlined"
+ density="compact"
required
/>
@@ -24,10 +24,15 @@
-
+
Close
-
+
Save
@@ -38,12 +43,13 @@
diff --git a/src/main.js b/src/main.js
index a7ec5376..e13a5edc 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,6 +1,5 @@
-import Vue from "vue";
-import Chat from "vue-beautiful-chat";
-import { createPinia, PiniaVuePlugin } from "pinia";
+import { createApp } from "vue";
+import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import App from "./App.vue";
@@ -9,17 +8,30 @@ import router from "./router";
import "@fontsource/roboto";
import vuetify from "./plugins/vuetify";
-Vue.use(Chat);
+const app = createApp(App);
-Vue.use(PiniaVuePlugin);
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
-Vue.use(pinia);
-Vue.config.productionTip = false;
-new Vue({
- vuetify,
- pinia,
- router,
- render: (h) => h(App),
-}).$mount("#app");
+app.config.errorHandler = (err, instance, info) => {
+ console.error(`[Starkiller] Unhandled error in ${info}:`, err);
+ try {
+ // eslint-disable-next-line no-underscore-dangle
+ const root = instance?.$?.appContext?.app?._instance;
+ const snack = root?.proxy?.$data?.snackProxy;
+ if (snack) {
+ snack.error(`Unexpected error: ${err.message || err}`);
+ }
+ } catch (secondaryErr) {
+ console.warn(
+ "[Starkiller] Could not display error notification:",
+ secondaryErr,
+ );
+ }
+};
+
+app.use(pinia);
+app.use(router);
+app.use(vuetify);
+
+app.mount("#app");
diff --git a/src/mixins/copy-stager.js b/src/mixins/copy-stager.js
index 6d3230b0..48226ea5 100644
--- a/src/mixins/copy-stager.js
+++ b/src/mixins/copy-stager.js
@@ -1,18 +1,14 @@
+import { copyToClipboard } from "@/utils/clipboard";
+
export default {
+ inject: ["snack"],
methods: {
/**
* Copies stager output to clipboard
* @param {*} output text to copy
*/
async copyStager(output) {
- try {
- await navigator.clipboard.writeText(output);
- this.$snack.success("Output copied to clipboard");
- } catch (error) {
- this.$snack.warn(
- "Failed to copy to clipboard. You must be on HTTPS or localhost.",
- );
- }
+ await copyToClipboard(output, this.snack);
},
},
};
diff --git a/src/plugins/vuetify.js b/src/plugins/vuetify.js
index 00eda32e..ff0cef35 100644
--- a/src/plugins/vuetify.js
+++ b/src/plugins/vuetify.js
@@ -1,22 +1,57 @@
-import Vue from "vue";
-import Vuetify from "vuetify/lib/framework";
-import colors from "vuetify/lib/util/colors";
-import PortalVue from "portal-vue";
-
+import { createVuetify } from "vuetify";
+import * as labsComponents from "vuetify/labs/components";
+import { aliases } from "vuetify/iconsets/fa";
+import "vuetify/styles";
import "@mdi/font/css/materialdesignicons.css";
import "@fortawesome/fontawesome-free/css/all.css";
+import { h } from "vue";
-Vue.use(PortalVue);
-Vue.use(Vuetify);
+// Custom icon component that auto-detects FA vs MDI based on icon name prefix
+const autoIconComponent = {
+ props: {
+ icon: { type: [String, Function, Object], required: true },
+ tag: { type: String, default: "i" },
+ },
+ render() {
+ if (typeof this.icon === "string") {
+ // MDI icons: mdi-bell-outline ->
+ if (this.icon.startsWith("mdi-")) {
+ return h(this.tag, { class: `mdi ${this.icon}` });
+ }
+ // Bare FA icons: fa-server ->
+ if (this.icon.startsWith("fa-")) {
+ return h(this.tag, { class: `fas ${this.icon}` });
+ }
+ // Full FA class strings from aliases: "fas fa-check" ->
+ return h(this.tag, { class: this.icon });
+ }
+ // For component icons, render as-is
+ return h(this.icon);
+ },
+};
-export default new Vuetify({
+export default createVuetify({
+ components: {
+ ...labsComponents,
+ },
theme: {
- dark: true,
+ defaultTheme: "dark",
themes: {
dark: {
- primary: colors.orange.darken2,
- secondary: colors.orange.lighten2,
- accent: colors.orange.base,
+ colors: {
+ primary: "#f57c00",
+ secondary: "#FFB74D",
+ accent: "#FF9800",
+ },
+ },
+ },
+ },
+ icons: {
+ defaultSet: "custom",
+ aliases,
+ sets: {
+ custom: {
+ component: autoIconComponent,
},
},
},
diff --git a/src/router/index.js b/src/router/index.js
index c50db135..5c3e0f66 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -1,10 +1,7 @@
-import Vue from "vue";
-import VueRouter from "vue-router";
+import { createRouter, createWebHashHistory } from "vue-router";
import { useApplicationStore } from "@/stores/application-module";
import Home from "../views/Home.vue";
-Vue.use(VueRouter);
-
const routes = [
{
path: "/",
@@ -180,9 +177,8 @@ const routes = [
},
];
-const router = new VueRouter({
- mode: "hash",
- base: import.meta.env.BASE_URL,
+const router = createRouter({
+ history: createWebHashHistory(import.meta.env.BASE_URL),
routes,
});
diff --git a/src/stores/agent-module.js b/src/stores/agent-module.js
index 679541ec..7f4fa4fe 100644
--- a/src/stores/agent-module.js
+++ b/src/stores/agent-module.js
@@ -14,17 +14,22 @@ export const useAgentStore = defineStore("agent", {
actions: {
async getAgents() {
this.status = "loading";
- const agents = await agentApi.getAgents(true);
- this.agents = agents;
- this.status = "success";
+ try {
+ const agents = await agentApi.getAgents(true);
+ this.agents = agents;
+ this.status = "success";
- const { autoSubscribeAgents } = useApplicationStore();
- if (autoSubscribeAgents) {
- agents.forEach((agent) => {
- if (!this.subscribed[agent.session_id]) {
- this.subscribe({ sessionId: agent.session_id });
- }
- });
+ const { autoSubscribeAgents } = useApplicationStore();
+ if (autoSubscribeAgents) {
+ agents.forEach((agent) => {
+ if (!this.subscribed[agent.session_id]) {
+ this.subscribe({ sessionId: agent.session_id });
+ }
+ });
+ }
+ } catch (err) {
+ console.error("[Starkiller] Failed to fetch agents:", err);
+ this.status = "error";
}
},
async getAgent({ sessionId }) {
diff --git a/src/stores/application-module.js b/src/stores/application-module.js
index 2cc10294..b2c1dfa1 100644
--- a/src/stores/application-module.js
+++ b/src/stores/application-module.js
@@ -5,6 +5,7 @@ import { setInstance } from "@/api/axios-instance";
// eslint-disable-next-line import/prefer-default-export
export const useApplicationStore = defineStore("application", {
persist: {
+ omit: ["chatUnreadCount"],
afterRestore: (ctx) => {
setInstance(ctx.store.url, ctx.store.token);
},
@@ -25,6 +26,7 @@ export const useApplicationStore = defineStore("application", {
taskHeaders: [],
pluginTaskHeaders: [],
connectionError: 0,
+ chatUnreadCount: 0,
notifications: [],
}),
actions: {
diff --git a/src/utils/clipboard.js b/src/utils/clipboard.js
new file mode 100644
index 00000000..e23ac7f5
--- /dev/null
+++ b/src/utils/clipboard.js
@@ -0,0 +1,12 @@
+// eslint-disable-next-line import/prefer-default-export
+export async function copyToClipboard(text, snack) {
+ try {
+ await navigator.clipboard.writeText(text);
+ snack.success("Copied to clipboard");
+ } catch (error) {
+ console.error("[Starkiller] Clipboard write failed:", error);
+ snack.warn(
+ "Failed to copy to clipboard. You must be on HTTPS or localhost.",
+ );
+ }
+}
diff --git a/src/utils/tags.js b/src/utils/tags.js
new file mode 100644
index 00000000..9e54ac18
--- /dev/null
+++ b/src/utils/tags.js
@@ -0,0 +1,16 @@
+import * as tagApi from "@/api/tag-api";
+
+// eslint-disable-next-line import/prefer-default-export
+export async function fetchDedupedTags(source) {
+ const tags = await tagApi.getTags({ page: 1, limit: -1, sources: source });
+ const dedupedTags = [];
+ tags.records.forEach((tag) => {
+ const existingTag = dedupedTags.find(
+ (t) => t.name === tag.name && t.value === tag.value,
+ );
+ if (!existingTag) {
+ dedupedTags.push(tag);
+ }
+ });
+ return dedupedTags;
+}
diff --git a/src/views/About.vue b/src/views/About.vue
index aac28ff0..3cd65c69 100644
--- a/src/views/About.vue
+++ b/src/views/About.vue
@@ -39,7 +39,7 @@ export default {
return {
breads: [
{
- text: "About",
+ title: "About",
disabled: true,
href: "/about",
},
diff --git a/src/views/AgentEdit.vue b/src/views/AgentEdit.vue
index 01fc45bb..2dfe9a05 100644
--- a/src/views/AgentEdit.vue
+++ b/src/views/AgentEdit.vue
@@ -1,28 +1,28 @@
-
+
-
-
+
+
Interact
- fa-arrow-pointer
+ fa-arrow-pointer
-
+
File Browser
- fa-folder-open
+ fa-folder-open
-
+
Tasks
- fa-sticky-note
+ fa-sticky-note
-
+
Jobs
- fa-cogs
+ fa-cogs
-
+
View
- fa-eye
+ fa-eye
@@ -33,13 +33,15 @@
-
-
+
+
-
-
- fa-user-cog
+
+
+
+ fa-user-cog
+
Elevated Process
@@ -68,59 +70,90 @@
:button-text="isRefreshTasks ? 'On' : 'Off'"
text="Auto-refresh Tasks"
/>
-
-
-
-
-
-
-
-
-
+
+
+
+ fa-ellipsis-v
+
+
+
+
+
+ fa-calendar-times
+
+ Clear Queued Tasks
+
+
+
+ fa-upload
+
+ Upload
+
+
+
+ fa-download
+
+ Download
+
+
+
+ fa-external-link-alt
+
+ Popout
+
+
+
+
+ fa-bell
+
+ Subscribe to Notifications
+
+
+
+ fa-bell-slash
+
+ Unsubscribe from Notifications
+
+
+
+ fa-sync
+
+ Reload SysInfo
+
+
+
+ fa-tasks
+
+ Get Agent Task Status List
+
+
+
+
+ fa-trash-alt
+
+ Kill Agent
+
+
+
-
+
-
-
+
-
-
- Form
- fa-list-check
+
+
+ Module
+
+ fa-grip-horizontal
+
-
+
+ Shell
+ fa-terminal
+
+
Terminal
- fa-terminal
+ fa-code
-
-
+
-
- Execute Module
+
+
-
-
+
-
-
+
+
-
+
This agent is archived.
-
-
+
@@ -192,13 +237,13 @@
-
-
+
@@ -209,20 +254,20 @@
:refresh-tasks="isRefreshTasks"
/>
-
-
+
-
-
+
@@ -233,8 +278,8 @@
@refresh-agent="getAgent(id)"
/>
-
-
+
+
-
+
-
@@ -103,9 +110,9 @@
@@ -117,7 +124,8 @@
-
+
-
-
-
+
+
@@ -163,14 +171,15 @@ export default {
EditPageTop,
AutoRunModules,
},
+ inject: ["snack", "confirm"],
data() {
return {
listener: { options: {} },
listenerTemplate: { options: {} },
selectedTemplate: "",
form: {},
- reset: true,
loading: false,
+ loadingTemplate: false,
formPriorities: ["Name", "Host", "Port"],
errorState: false,
validationMessage: null,
@@ -195,7 +204,7 @@ export default {
return this.$route.name === "listenerNew";
},
isCopy() {
- return this.$route.params.copy === true;
+ return this.$route.query.copy === "true";
},
mode() {
if (this.isCopy) return "Copy";
@@ -206,11 +215,11 @@ export default {
return this.isNew || !this.listener.enabled;
},
id() {
- return this.isCopy ? 0 : this.$route.params.id;
+ return this.isCopy ? 0 : this.$route.params.id || this.$route.query.id;
},
copyLink() {
if (this.id > 0)
- return { name: "listenerNew", params: { copy: true, id: this.id } };
+ return { name: "listenerNew", query: { copy: true, id: this.id } };
return {};
},
listenerInfo() {
@@ -231,22 +240,27 @@ export default {
});
return options;
}
- const { options } = this.listenerTemplate;
- if (!options) return {};
+ const templateOptions =
+ (this.listenerTemplate && this.listenerTemplate.options) || {};
+ const options = Object.keys(templateOptions).reduce((acc, k) => {
+ acc[k] = { ...templateOptions[k] };
+ return acc;
+ }, {});
+ if (Object.keys(options).length === 0) return {};
return options;
},
breads() {
return [
{
- text: "Listeners",
+ title: "Listeners",
disabled: false,
to: "/listeners",
exact: true,
},
{
- text: this.breadcrumbName,
+ title: this.breadcrumbName,
disabled: true,
- to: "/listeners-edit",
+ to: `/listeners/${this.id}`,
},
];
},
@@ -268,15 +282,17 @@ export default {
watch: {
selectedTemplate: {
async handler(val) {
+ this.loadingTemplate = true;
const a = await listenerApi
.getListenerTemplate(val)
- .catch((err) => this.$snack.error(`Error: ${err}`));
+ .catch((err) =>
+ this.snack.error(
+ `Error: ${err?.response?.data?.detail || err?.message || err}`,
+ ),
+ );
+ this.loadingTemplate = false;
if (a) {
- this.reset = false;
-
this.listenerTemplate = a;
- await this.$nextTick();
- this.reset = true;
this.initialLoad = true;
}
},
@@ -293,7 +309,7 @@ export default {
if (!this.isNew || this.isCopy) {
// using the route param id instead of this.id
// since this.id is 0 for copies.
- this.getListener(this.$route.params.id);
+ this.getListener(this.$route.params.id || this.$route.query.id);
}
},
methods: {
@@ -305,7 +321,7 @@ export default {
(t) => t.id !== tag.id,
);
})
- .catch((err) => this.$snack.error(`Error: ${err}`));
+ .catch((err) => this.snack.error(`Error: ${err}`));
},
updateTag(tag) {
listenerApi
@@ -313,9 +329,9 @@ export default {
.then((t) => {
const index = this.listener.tags.findIndex((x) => x.id === t.id);
this.listener.tags.splice(index, 1, t);
- this.$snack.success("Tag updated");
+ this.snack.success("Tag updated");
})
- .catch((err) => this.$snack.error(`Error: ${err}`));
+ .catch((err) => this.snack.error(`Error: ${err}`));
},
addTag(tag) {
listenerApi
@@ -323,26 +339,26 @@ export default {
.then((t) => {
this.listener.tags.push(t);
})
- .catch((err) => this.$snack.error(`Error: ${err}`));
+ .catch((err) => this.snack.error(`Error: ${err}`));
},
async submit() {
- if (this.loading || !this.$refs.generalform.$refs.form.validate()) {
- return;
- }
+ if (this.loading) return;
+ const valid = await this.$refs.generalform.validate();
+ if (!valid) return;
this.loading = true;
if (this.id > 0) {
listenerApi
.updateListener({ ...this.listener, options: this.form })
.then(() => {
- this.$snack.success("Listener updated");
+ this.snack.success("Listener updated");
this.loading = false;
})
.catch((err) => {
if (err.startsWith("[*]")) {
this.validationMessage = err;
} else {
- this.$snack.error(`Error: ${err}`);
+ this.snack.error(`Error: ${err}`);
}
this.loading = false;
});
@@ -350,7 +366,7 @@ export default {
listenerApi
.createListener(this.selectedTemplate, this.form)
.then(({ id }) => {
- this.$snack.success("Listener created");
+ this.snack.success("Listener created");
this.loading = false;
this.$router.push({ name: "listenerEdit", params: { id } });
})
@@ -358,7 +374,7 @@ export default {
if (err.startsWith("[*]")) {
this.validationMessage = err;
} else {
- this.$snack.error(`Error: ${err}`);
+ this.snack.error(`Error: ${err}`);
}
this.loading = false;
});
@@ -366,7 +382,7 @@ export default {
},
async kill() {
if (
- await this.$root.$confirm(
+ await this.confirm(
"Delete",
`Are you sure you want to kill listener ${this.form.Name}?`,
{ color: "red" },
@@ -376,7 +392,7 @@ export default {
await this.listenerStore.killListener(this.id);
this.$router.push({ name: "listeners" });
} catch (err) {
- this.$snack.error(`Error: ${err}`);
+ this.snack.error(`Error: ${err}`);
}
}
},
@@ -390,7 +406,9 @@ export default {
});
this.selectedTemplate = data.template;
})
- .catch(() => {
+ .catch((err) => {
+ console.error(err);
+ this.snack.error(`Failed to load resource: ${err}`);
this.errorState = true;
});
},
@@ -399,7 +417,7 @@ export default {
if (
val === true &&
- !(await this.$root.$confirm(
+ !(await this.confirm(
"",
"Re-enabling the listener will also save any unsaved option changes.",
{ color: "yellow" },
@@ -417,18 +435,9 @@ export default {
this.listener = response;
} catch (err) {
this.listener.enabled = !val;
- this.$snack.error(`Error: ${err}`);
+ this.snack.error(`Error: ${err}`);
}
},
},
};
-
-
diff --git a/src/views/MalleableProfileEdit.vue b/src/views/MalleableProfileEdit.vue
index 1c9a9b11..b03a1907 100644
--- a/src/views/MalleableProfileEdit.vue
+++ b/src/views/MalleableProfileEdit.vue
@@ -26,14 +26,14 @@
ref="form"
v-model="valid"
style="max-width: 500px"
- @submit.prevent.native="submit"
+ @submit.prevent="submit"
>
@@ -41,8 +41,8 @@
v-model="form.category"
:rules="rules['category']"
label="category"
- outlined
- dense
+ variant="outlined"
+ density="compact"
required
:disabled="!isNew"
/>
@@ -50,8 +50,8 @@
v-model="form.data"
:rules="rules['code']"
label="code"
- outlined
- dense
+ variant="outlined"
+ density="compact"
required
auto-grow
/>
@@ -61,7 +61,6 @@
-
-
diff --git a/src/views/Settings.vue b/src/views/Settings.vue
index 0c832613..3022a7e0 100644
--- a/src/views/Settings.vue
+++ b/src/views/Settings.vue
@@ -23,12 +23,13 @@
{{ user.username }}
- Logout
+ Logout
@@ -41,7 +42,7 @@
ref="form"
v-model="valid"
style="max-width: 500px"
- @submit.prevent.native="submit"
+ @submit.prevent="submit"
>
@@ -62,8 +63,8 @@
:rules="rules['confirmPassword']"
label="Confirm Password"
autocomplete="off"
- outlined
- dense
+ variant="outlined"
+ density="compact"
required
@click:append="showConfirm = !showConfirm"
/>
@@ -215,6 +216,7 @@ export default {
components: {
ListPageTop,
},
+ inject: ["snack", "confirm"],
data() {
return {
password: {
@@ -250,7 +252,7 @@ export default {
valid: false,
breads: [
{
- text: "Settings",
+ title: "Settings",
disabled: true,
href: "/settings",
},
@@ -307,13 +309,17 @@ export default {
const data = new FormData();
data.append("file", selectedFile);
- await userApi.uploadAvatar(this.userId, data);
- this.applicationStore.refreshMe();
- this.$snack.success("Upload complete");
+ try {
+ await userApi.uploadAvatar(this.userId, data);
+ this.applicationStore.refreshMe();
+ this.snack.success("Upload complete");
+ } catch (err) {
+ this.snack.error(`Error uploading avatar: ${err}`);
+ }
},
async logout() {
if (
- await this.$root.$confirm("", "Are you sure you want to logout?", {
+ await this.confirm("", "Are you sure you want to logout?", {
color: "green",
})
) {
@@ -324,21 +330,21 @@ export default {
this.applicationStore.clear();
this.agentStore.clear();
},
- submit() {
- if (this.password.loading || !this.$refs.form.validate()) {
- return;
- }
+ async submit() {
+ if (this.password.loading) return;
+ const { valid } = await this.$refs.form.validate();
+ if (!valid) return;
this.password.loading = true;
userApi
.updatePassword(this.user.id, this.password.form.password)
.then(() => {
- this.$snack.success("Password updated");
+ this.snack.success("Password updated");
this.password.form = {};
this.$refs.form.resetValidation();
})
.catch((err) => {
- this.$snack.error(`Error: ${err}`);
+ this.snack.error(`Error: ${err}`);
})
.finally(() => {
this.password.loading = false;
@@ -349,10 +355,10 @@ export default {
malleableApi
.resetProfiles()
.then(() => {
- this.$snack.success("Profiles reset successful");
+ this.snack.success("Profiles reset successful");
})
.catch((err) => {
- this.$snack.error(`Error: ${err}`);
+ this.snack.error(`Error: ${err}`);
})
.finally(() => {
this.profiles.loading = false;
@@ -363,10 +369,10 @@ export default {
malleableApi
.reloadProfiles()
.then(() => {
- this.$snack.success("Profiles reload successful");
+ this.snack.success("Profiles reload successful");
})
.catch((err) => {
- this.$snack.error(`Error: ${err}`);
+ this.snack.error(`Error: ${err}`);
})
.finally(() => {
this.profiles.loading = false;
@@ -377,10 +383,10 @@ export default {
moduleApi
.reloadModules()
.then(() => {
- this.$snack.success("Module reload successful");
+ this.snack.success("Module reload successful");
})
.catch((err) => {
- this.$snack.error(`Error: ${err}`);
+ this.snack.error(`Error: ${err}`);
})
.finally(() => {
this.modules.loading = false;
@@ -391,10 +397,10 @@ export default {
moduleApi
.resetModules()
.then(() => {
- this.$snack.success("Module reset successful");
+ this.snack.success("Module reset successful");
})
.catch((err) => {
- this.$snack.error(`Error: ${err}`);
+ this.snack.error(`Error: ${err}`);
})
.finally(() => {
this.modules.loading = false;
@@ -405,10 +411,10 @@ export default {
bypassApi
.reloadBypasses()
.then(() => {
- this.$snack.success("Bypass reload successful");
+ this.snack.success("Bypass reload successful");
})
.catch((err) => {
- this.$snack.error(`Error: ${err}`);
+ this.snack.error(`Error: ${err}`);
})
.finally(() => {
this.bypasses.loading = false;
@@ -419,10 +425,10 @@ export default {
bypassApi
.resetBypasses()
.then(() => {
- this.$snack.success("Bypass reset successful");
+ this.snack.success("Bypass reset successful");
})
.catch((err) => {
- this.$snack.error(`Error: ${err}`);
+ this.snack.error(`Error: ${err}`);
})
.finally(() => {
this.bypasses.loading = false;
@@ -433,10 +439,10 @@ export default {
pluginApi
.reloadPlugins()
.then(() => {
- this.$snack.success("Plugin reload successful");
+ this.snack.success("Plugin reload successful");
})
.catch((err) => {
- this.$snack.error(`Error: ${err}`);
+ this.snack.error(`Error: ${err}`);
})
.finally(() => {
this.plugins.loading = false;
diff --git a/src/views/StagerEdit.vue b/src/views/StagerEdit.vue
index 2b0a87ff..8a678731 100644
--- a/src/views/StagerEdit.vue
+++ b/src/views/StagerEdit.vue
@@ -43,9 +43,9 @@
@@ -53,20 +53,21 @@
-
+
A name for the stager. Leave blank for an autogenerated name.
-
+
0)
- return { name: "stagerNew", params: { copy: true, id: this.id } };
+ return { name: "stagerNew", query: { copy: true, id: this.id } };
return {};
},
stagerInfo() {
@@ -184,15 +185,15 @@ export default {
breads() {
return [
{
- text: "Stagers",
+ title: "Stagers",
disabled: false,
to: "/stagers",
exact: true,
},
{
- text: this.breadcrumbName,
+ title: this.breadcrumbName,
disabled: true,
- to: "/stagers-edit",
+ to: `/stagers/${this.id}`,
},
];
},
@@ -211,13 +212,9 @@ export default {
async handler(val) {
const a = await stagerApi
.getStagerTemplate(val)
- .catch((err) => this.$snack.error(`Error: ${err}`));
+ .catch((err) => this.snack.error(`Error: ${err}`));
if (a) {
- this.reset = false;
-
this.stagerTemplate = a;
- await this.$nextTick();
- this.reset = true;
this.initialLoad = true;
}
},
@@ -234,7 +231,7 @@ export default {
if (!this.isNew || this.isCopy) {
// using the route param id instad of this.id
// since this.id is 0 for copies.
- this.getStager(this.$route.params.id);
+ this.getStager(this.$route.params.id || this.$route.query.id);
}
if (this.$route.query.template) {
@@ -242,26 +239,25 @@ export default {
}
},
methods: {
- submit() {
- if (this.loading || !this.$refs.generalform.$refs.form.validate()) {
- return;
- }
+ async submit() {
+ if (this.loading) return;
+ const valid = await this.$refs.generalform.validate();
+ if (!valid) return;
this.loading = true;
if (this.id > 0) {
- stagerApi
- .updateStager(this.id, { name: this.stager.name, options: this.form })
- .then(() => {
- this.$snack.success("Stager updated");
- this.loading = false;
- })
- .then(() => {
- this.getStager(this.id);
- })
- .catch((err) => {
- this.$snack.error(`Error: ${err}`);
- this.loading = false;
+ try {
+ await stagerApi.updateStager(this.id, {
+ name: this.stager.name,
+ options: this.form,
});
+ this.snack.success("Stager updated");
+ this.loading = false;
+ this.getStager(this.id);
+ } catch (err) {
+ this.snack.error(`Error: ${err}`);
+ this.loading = false;
+ }
} else {
stagerApi
.createStager(
@@ -270,19 +266,19 @@ export default {
this.form,
)
.then(({ id }) => {
- this.$snack.success("Stager created");
+ this.snack.success("Stager created");
this.loading = false;
this.$router.push({ name: "stagerEdit", params: { id } });
})
.catch((err) => {
- this.$snack.error(`Error: ${err}`);
+ this.snack.error(`Error: ${err}`);
this.loading = false;
});
}
},
async deleteStager() {
if (
- await this.$root.$confirm(
+ await this.confirm(
"Delete",
`Are you sure you want to delete stager ${this.form.StarkillerName}?`,
{ color: "red" },
@@ -292,7 +288,7 @@ export default {
await this.stagerStore.deleteStager(this.id);
this.$router.push({ name: "stagers" });
} catch (err) {
- this.$snack.error(`Error: ${err}`);
+ this.snack.error(`Error: ${err}`);
}
}
},
@@ -303,7 +299,9 @@ export default {
this.stager = data;
this.selectedTemplate = data.template;
})
- .catch(() => {
+ .catch((err) => {
+ console.error(err);
+ this.snack.error(`Failed to load resource: ${err}`);
this.errorState = true;
});
},
diff --git a/src/views/Stagers.vue b/src/views/Stagers.vue
index a3c24201..4b1a5d5d 100644
--- a/src/views/Stagers.vue
+++ b/src/views/Stagers.vue
@@ -13,6 +13,7 @@
@@ -29,7 +30,6 @@