diff --git a/src/attack_flow_builder/src/App.vue b/src/attack_flow_builder/src/App.vue index 639bfaf0..1a3cf6fb 100644 --- a/src/attack_flow_builder/src/App.vue +++ b/src/attack_flow_builder/src/App.vue @@ -4,6 +4,7 @@
+
@@ -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 @@ 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 +};