@@ -23,14 +24,16 @@ import Configuration from "@/assets/builder.config"
import { clamp } from "./assets/scripts/BlockDiagram";
import { PointerTracker } from "./assets/scripts/PointerTracker";
import { mapMutations, mapState } from 'vuex';
-import { LoadFile, LoadSettings } from './store/Commands/AppCommands';
+import * as App from './store/Commands/AppCommands';
import { defineComponent, markRaw, ref } from 'vue';
// Components
+import SplashMenu from "@/components/Controls/SplashMenu.vue";
import AppTitleBar from "@/components/Elements/AppTitleBar.vue";
import AppHotkeyBox from "@/components/Elements/AppHotkeyBox.vue";
import BlockDiagram from "@/components/Elements/BlockDiagram.vue";
import AppFooterBar from "@/components/Elements/AppFooterBar.vue";
import EditorSidebar from "@/components/Elements/EditorSidebar.vue";
+import { ShowSplashMenu } from "./store/Commands/AppCommands/ShowSplashMenu";
const Handle = {
None : 0,
@@ -156,20 +159,22 @@ export default defineComponent({
settings = require("../public/settings.json");
}
// Load settings
- this.execute(new LoadSettings(this.context, settings));
+ this.execute(new App.LoadSettings(this.context, settings));
// Load empty file
- this.execute(await LoadFile.fromNew(this.context));
+ this.execute(await App.LoadFile.fromNew(this.context));
// Load file from query parameters, if possible
let params = new URLSearchParams(window.location.search);
let src = params.get("src");
if(src) {
try {
// TODO: Incorporate loading dialog
- this.execute(await LoadFile.fromUrl(this.context, src));
+ this.execute(await App.LoadFile.fromUrl(this.context, src));
} catch(ex) {
console.error(`Failed to load file from url: '${ src }'`);
console.error(ex);
}
+ } else {
+ this.execute(new ShowSplashMenu(this.context));
}
},
mounted() {
@@ -193,7 +198,8 @@ export default defineComponent({
AppTitleBar,
BlockDiagram,
AppFooterBar,
- EditorSidebar
+ EditorSidebar,
+ SplashMenu,
},
});
diff --git a/src/attack_flow_builder/src/assets/afb.png b/src/attack_flow_builder/src/assets/afb.png
new file mode 100644
index 00000000..c371c7f6
Binary files /dev/null and b/src/attack_flow_builder/src/assets/afb.png differ
diff --git a/src/attack_flow_builder/src/assets/builder.config.ts b/src/attack_flow_builder/src/assets/builder.config.ts
index 1c2e06a8..acd5c420 100644
--- a/src/attack_flow_builder/src/assets/builder.config.ts
+++ b/src/attack_flow_builder/src/assets/builder.config.ts
@@ -24,6 +24,35 @@ const config: AppConfiguration = {
application_name: "Attack Flow Builder",
file_type_name: "Attack Flow",
file_type_extension: "afb",
+ menu_icon: "./ctid-small.png",
+ splash: {
+ product: "./afb.png",
+ organization: "./ctid.png",
+ buttons: [
+ {
+ action: "new",
+ name: "New Flow",
+ description: "Create a new, blank flow",
+ },
+ {
+ action: "open",
+ name: "Open Flow",
+ description: "Open an existing flow from your computer"
+ },
+ {
+ action: "link",
+ name: "Example Flows",
+ description: "View a list of example flows",
+ url: "https://center-for-threat-informed-defense.github.io/attack-flow/example_flows/"
+ },
+ {
+ action: "link",
+ name: "Builder Help",
+ description: "View help for Attack Flow Builder",
+ url: "https://center-for-threat-informed-defense.github.io/attack-flow/builder/"
+ },
+ ],
+ },
schema: {
page_template: "flow",
templates: [
diff --git a/src/attack_flow_builder/src/assets/ctid-small.png b/src/attack_flow_builder/src/assets/ctid-small.png
new file mode 100644
index 00000000..546d94f8
Binary files /dev/null and b/src/attack_flow_builder/src/assets/ctid-small.png differ
diff --git a/src/attack_flow_builder/src/assets/ctid.png b/src/attack_flow_builder/src/assets/ctid.png
new file mode 100755
index 00000000..a93f1ed8
Binary files /dev/null and b/src/attack_flow_builder/src/assets/ctid.png differ
diff --git a/src/attack_flow_builder/src/components/Controls/SplashMenu.vue b/src/attack_flow_builder/src/components/Controls/SplashMenu.vue
new file mode 100644
index 00000000..233d0fc3
--- /dev/null
+++ b/src/attack_flow_builder/src/components/Controls/SplashMenu.vue
@@ -0,0 +1,188 @@
+
+
+
+
+
+
+
diff --git a/src/attack_flow_builder/src/components/Elements/AppTitleBar.vue b/src/attack_flow_builder/src/components/Elements/AppTitleBar.vue
index fabe6f1b..d66ca54f 100644
--- a/src/attack_flow_builder/src/components/Elements/AppTitleBar.vue
+++ b/src/attack_flow_builder/src/components/Elements/AppTitleBar.vue
@@ -1,12 +1,14 @@
- AFB
+
diff --git a/src/attack_flow_builder/src/components/Icons/Link.vue b/src/attack_flow_builder/src/components/Icons/Link.vue
new file mode 100644
index 00000000..bcafff3e
--- /dev/null
+++ b/src/attack_flow_builder/src/components/Icons/Link.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/src/attack_flow_builder/src/components/Icons/NewFlow.vue b/src/attack_flow_builder/src/components/Icons/NewFlow.vue
new file mode 100644
index 00000000..b792a971
--- /dev/null
+++ b/src/attack_flow_builder/src/components/Icons/NewFlow.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/src/attack_flow_builder/src/components/Icons/OpenFlow.vue b/src/attack_flow_builder/src/components/Icons/OpenFlow.vue
new file mode 100644
index 00000000..f30eab00
--- /dev/null
+++ b/src/attack_flow_builder/src/components/Icons/OpenFlow.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/src/attack_flow_builder/src/store/Commands/AppCommands/HideSplashMenu.ts b/src/attack_flow_builder/src/store/Commands/AppCommands/HideSplashMenu.ts
new file mode 100644
index 00000000..36f7fdb2
--- /dev/null
+++ b/src/attack_flow_builder/src/store/Commands/AppCommands/HideSplashMenu.ts
@@ -0,0 +1,22 @@
+import { AppCommand } from "../AppCommand";
+import { ApplicationStore } from "@/store/StoreTypes";
+
+export class HideSplashMenu extends AppCommand {
+
+ /**
+ * Display the find dialog
+ * @param context
+ * The application context.
+ */
+ constructor(context: ApplicationStore) {
+ super(context);
+ }
+
+ /**
+ * Executes the command.
+ */
+ public execute(): void {
+ this._context.splashIsVisible = false;
+ }
+
+}
diff --git a/src/attack_flow_builder/src/store/Commands/AppCommands/ShowSplashMenu.ts b/src/attack_flow_builder/src/store/Commands/AppCommands/ShowSplashMenu.ts
new file mode 100644
index 00000000..598f5a4e
--- /dev/null
+++ b/src/attack_flow_builder/src/store/Commands/AppCommands/ShowSplashMenu.ts
@@ -0,0 +1,22 @@
+import { AppCommand } from "../AppCommand";
+import { ApplicationStore } from "@/store/StoreTypes";
+
+export class ShowSplashMenu extends AppCommand {
+
+ /**
+ * Display the find dialog
+ * @param context
+ * The application context.
+ */
+ constructor(context: ApplicationStore) {
+ super(context);
+ }
+
+ /**
+ * Executes the command.
+ */
+ public execute(): void {
+ this._context.splashIsVisible = true;
+ }
+
+}
diff --git a/src/attack_flow_builder/src/store/Commands/AppCommands/index.ts b/src/attack_flow_builder/src/store/Commands/AppCommands/index.ts
index b70eb4aa..4598bc6d 100644
--- a/src/attack_flow_builder/src/store/Commands/AppCommands/index.ts
+++ b/src/attack_flow_builder/src/store/Commands/AppCommands/index.ts
@@ -1,5 +1,6 @@
export * from "./ClearPageRecoveryBank";
export * from "./CopySelectedChildren";
+export * from "./HideSplashMenu";
export * from "./LoadFile";
export * from "./LoadSettings";
export * from "./NullCommand";
@@ -11,6 +12,7 @@ export * from "./SaveSelectionImageToDevice";
export * from "./SetEditorPointerLocation";
export * from "./SetEditorViewParams";
export * from "./SetRenderQuality";
+export * from "./ShowSplashMenu";
export * from "./SwitchToFullscreen";
export * from "./ToggleDebugDisplay";
export * from "./ToggleGridDisplay";
diff --git a/src/attack_flow_builder/src/store/StoreTypes.ts b/src/attack_flow_builder/src/store/StoreTypes.ts
index 05f65ac4..b8f9e974 100644
--- a/src/attack_flow_builder/src/store/StoreTypes.ts
+++ b/src/attack_flow_builder/src/store/StoreTypes.ts
@@ -29,7 +29,8 @@ export type ApplicationStore = {
clipboard: DiagramObjectModel[],
publisher: DiagramPublisher | undefined,
activePage: PageEditor,
- recoveryBank: PageRecoveryBank
+ recoveryBank: PageRecoveryBank,
+ splashIsVisible: boolean,
}
/**
@@ -235,6 +236,24 @@ export type SelectHotkeys = {
// 3. App Configuration /////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
+/**
+ * Actions that a splash button can execute.
+ */
+export enum SplashButtonAction {
+ New = "new",
+ Open = "open",
+ Link = "link",
+}
+
+/**
+ * Configuration for a splash button.
+ */
+export type SplashButton = {
+ action: string;
+ name: string;
+ description: string;
+ url?: string;
+}
/**
* App Configuration File
@@ -242,8 +261,14 @@ export type SelectHotkeys = {
export type AppConfiguration = {
is_web_hosted: boolean,
application_name: string,
+ splash: {
+ product: string,
+ organization: string,
+ buttons: Array
,
+ },
file_type_name: string,
file_type_extension: string,
+ menu_icon: string,
schema: BlockDiagramSchema,
menus: {
help_menu: {
diff --git a/src/attack_flow_builder/src/store/Stores/ApplicationStore.ts b/src/attack_flow_builder/src/store/Stores/ApplicationStore.ts
index 8740d06d..eb7bbc4e 100644
--- a/src/attack_flow_builder/src/store/Stores/ApplicationStore.ts
+++ b/src/attack_flow_builder/src/store/Stores/ApplicationStore.ts
@@ -18,7 +18,8 @@ export default {
clipboard: [],
publisher: Publisher,
activePage: PageEditor.createDummy(),
- recoveryBank: new PageRecoveryBank()
+ recoveryBank: new PageRecoveryBank(),
+ splashIsVisible: false,
},
getters: {
@@ -122,7 +123,18 @@ export default {
let p = state.activePage;
// Use trigger to trip the reactivity system
return (state.activePage.trigger.value ? p : p).getValidationWarnings();
- }
+ },
+
+ /**
+ * Indicates whether the splash menu is visible.
+ * @param state
+ * The Vuex state.
+ * @returns
+ * True if the splash menu is visible.
+ */
+ isShowingSplash(state): boolean {
+ return state.splashIsVisible;
+ },
},
mutations: {
diff --git a/src/attack_flow_builder/vue.config.js b/src/attack_flow_builder/vue.config.js
index e015aca8..32a1468b 100644
--- a/src/attack_flow_builder/vue.config.js
+++ b/src/attack_flow_builder/vue.config.js
@@ -7,5 +7,11 @@ module.exports = {
"~": path.resolve(__dirname, "./")
}
}
+ },
+ chainWebpack: config => {
+ config.plugin("html").tap(args => {
+ args[0].title = "Attack Flow Builder";
+ return args;
+ })
}
-};
\ No newline at end of file
+};