Skip to content

Commit

Permalink
Merge pull request #99 from center-for-threat-informed-defense/AF-147…
Browse files Browse the repository at this point in the history
…-search-inside-flow

AF-147: Search inside a flow
  • Loading branch information
mikecarenzo committed Aug 28, 2023
2 parents aee44e9 + 6233ed3 commit 43bf696
Show file tree
Hide file tree
Showing 17 changed files with 689 additions and 5 deletions.
3 changes: 3 additions & 0 deletions src/attack_flow_builder/public/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
"paste": "Control+V",
"delete": "Backspace",
"duplicate": "Control+D",
"find": "Control+F",
"find_next": "F3",
"find_previous": "Shift+F3",
"select_all": "Control+A"
},
"layout": {
Expand Down
22 changes: 18 additions & 4 deletions src/attack_flow_builder/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<template>
<AppHotkeyBox id="main">
<AppTitleBar id="app-title-bar"/>
<FindDialog ref="findDialog" id="find-dialog" :style="findDialogLayout" />
<div id="app-body" ref="body" :style="gridLayout">
<div class="frame center">
<BlockDiagram id="block-diagram"/>
Expand All @@ -18,13 +19,13 @@
</template>

<script lang="ts">
import * as App from './store/Commands/AppCommands';
import * as Store from "@/store/StoreTypes";
import Configuration from "@/assets/builder.config"
// Dependencies
import { clamp } from "./assets/scripts/BlockDiagram";
import { PointerTracker } from "./assets/scripts/PointerTracker";
import { mapMutations, mapState } from 'vuex';
import * as App from './store/Commands/AppCommands';
import { defineComponent, markRaw, ref } from 'vue';
// Components
import SplashMenu from "@/components/Controls/SplashMenu.vue";
Expand All @@ -33,7 +34,7 @@ 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";
import FindDialog from "@/components/Elements/FindDialog.vue";
const Handle = {
None : 0,
Expand Down Expand Up @@ -84,6 +85,18 @@ export default defineComponent({
return {
gridTemplateColumns: `minmax(0, 1fr) ${ r }px`
}
},
/**
* Compute the location of the find dialog
* @returns
* The current grid layout.
*/
findDialogLayout(): { right: string } {
let r = this.frameSize[Handle.Right] + 25;
return {
right: `${r}px`
}
}
},
Expand Down Expand Up @@ -174,7 +187,7 @@ export default defineComponent({
console.error(ex);
}
} else {
this.execute(new ShowSplashMenu(this.context));
this.execute(new App.ShowSplashMenu(this.context));
}
},
mounted() {
Expand All @@ -199,7 +212,8 @@ export default defineComponent({
BlockDiagram,
AppFooterBar,
EditorSidebar,
SplashMenu,
FindDialog,
SplashMenu
},
});
</script>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export default class Debouncer {
private timer: number | null;
private seconds: number;

/**
* Creates a {@link Debouncer}.
* @param duration
* The number of seconds that the debouncer waits before calling its target function.
*/
constructor(seconds: number) {
this.timer = null;
this.seconds = seconds;
}

/**
* Debounce a function call.
* @param fn
* The function to call. This is only called if not superseded by another call before the debounce
* duration elapses.
*/
public call(fn: TimerHandler) {
if (this.timer !== null) {
clearTimeout(this.timer);
}

this.timer = setTimeout(fn, this.seconds * 1000);
}
}
234 changes: 234 additions & 0 deletions src/attack_flow_builder/src/components/Elements/FindDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
<template>
<div class="find-dialog-container" :class="{
hidden:
!isShowingFindDialog
}">
<input type="text" class="query" placeholder="Find" v-model="query" @keyup="runQuery" ref="query">
<div class="results">
<span v-if="totalResults === 0">No results</span>
<span v-if="totalResults !== 0">{{ currentResultIndex + 1 }} of {{ totalResults }}</span>
</div>
<div class="control" @pointerdown="findPrevious()" :class="{
disabled:
totalResults === 0
}">
<UpArrow />
</div>
<div class="control" @pointerdown="findNext()" :class="{
disabled:
totalResults === 0
}">
<DownArrow />
</div>
<div class="control" @pointerdown="hideFindDialog()">
<Close color="#8c8c8c" />
</div>
</div>
</template>

<script lang="ts">
import * as Store from "@/store/StoreTypes";
// Dependencies
import { defineComponent } from "vue";
import { mapGetters, mapMutations, mapState } from "vuex";
import * as App from "@/store/Commands/AppCommands";
import * as Page from "@/store/Commands/PageCommands";
import Debouncer from "@/assets/scripts/BlockDiagram/Utilities/Debouncer";
// Components
import Close from "@/components/Icons/Close.vue";
import DownArrow from "@/components/Icons/DownArrow.vue";
import UpArrow from "@/components/Icons/UpArrow.vue";
export default defineComponent({
name: "FindDialog",
data() {
return {
query: "",
lastQuery: "",
debouncer: new Debouncer(0.4),
totalResults: 0,
currentResultIndex: 0,
currentDiagramObject: null as any,
}
},
computed: {
...mapState("ApplicationStore", {
ctx(state: Store.ApplicationStore): Store.ApplicationStore {
return state;
},
editor(state: Store.ApplicationStore): Store.PageEditor {
return state.activePage;
}
}),
...mapGetters("ApplicationStore", ["isShowingFindDialog", "currentFindResult"]),
},
components: {
Close,
DownArrow,
UpArrow,
},
watch: {
isShowingFindDialog: {
handler(isShowingFindDialog, oldValue) {
if (isShowingFindDialog) {
this.focus();
} else {
this.blur();
}
}
},
currentFindResult: {
handler(newResult, oldResult) {
if (newResult !== null) {
this.currentResultIndex = newResult.index;
this.totalResults = newResult.totalResults;
if (this.currentDiagramObject !== newResult.diagramObject) {
this.currentDiagramObject = newResult.diagramObject;
this.execute(new Page.UnselectDescendants(this.editor.page));
this.execute(new Page.SelectObject(this.currentDiagramObject));
this.execute(new Page.MoveCameraToSelection(this.ctx, this.editor.page))
}
} else {
this.totalResults = 0;
}
}
}
},
methods: {
/**
* Application Store mutations
*/
...mapMutations("ApplicationStore", ["execute"]),
/**
* Focus the input field and highlight existing text
*/
focus() {
const queryInput = this.$refs.query as HTMLInputElement;
queryInput.focus();
queryInput.select();
},
/**
* Blur the input field.
*/
blur() {
const queryInput = this.$refs.query as HTMLInputElement;
queryInput.blur();
},
/**
* Update find results with the current query.
*/
runQuery(event: KeyboardEvent) {
if (event.key === "Escape") {
this.hideFindDialog();
} else if (event.key === "Enter") {
if (event.shiftKey) {
this.execute(new App.MoveToPreviousFindResult(this.ctx));
} else {
this.execute(new App.MoveToNextFindResult(this.ctx));
}
} else if (this.query !== this.lastQuery) {
this.debouncer.call(() => {
this.lastQuery = this.query;
this.ctx.finder.runQuery(this.ctx.activePage, this.query);
});
}
},
/**
* Focus the next item in the result set.
*/
findNext() {
this.execute(new App.MoveToNextFindResult(this.ctx));
},
/**
* Focus the previous item in the result set.
*/
findPrevious() {
this.execute(new App.MoveToPreviousFindResult(this.ctx));
},
/**
* Hide the find dialog.
*/
hideFindDialog() {
this.execute(new App.HideFindDialog(this.ctx));
},
}
});
</script>

<style scoped>
.find-dialog-container {
position: absolute;
top: 31px;
transition: top 0.3s ease-out;
z-index: 1;
overflow: hidden;
display: flex;
align-items: center;
background-color: #242424;
border: solid 1px #303030;
color: #d9d9d9;
padding: 2px 5px;
}
.hidden {
/* visibility: hidden; */
top: -13px;
}
.query {
color: #d9d9d9;
background-color: #2e2e2e;
border: 1px solid #3d3d3d;
border-radius: 2px;
height: 20px;
width: 180px;
margin: 6px 10px 6px 6px;
padding: 2px 5px;
}
.query:focus {
outline: none;
}
.results {
white-space: nowrap;
font-size: 10pt;
margin-right: 1em;
width: 60px;
}
.control {
font-size: 14pt;
/* margin: 0; */
border-radius: 4px;
padding: 2px 5px;
}
.control * {
fill: #8c8c8c;
}
.control.disabled:hover {
background: none;
}
.control.disabled * {
fill: #3d3d3d;
}
.control:hover {
background-color: #383838;
}
.control * {
position: relative;
top: 1px;
}
</style>
20 changes: 20 additions & 0 deletions src/attack_flow_builder/src/components/Icons/Close.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template>
<svg width="16" height="16" viewBox="0 0 384 512" :fill="color"
xmlns="http://www.w3.org/2000/svg"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path
d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z" />
</svg>
</template>

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "Close",
props: {
color: {
type: String,
default: "#737373"
}
}
});
</script>
20 changes: 20 additions & 0 deletions src/attack_flow_builder/src/components/Icons/DownArrow.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template>
<svg width="16" height="16" viewBox="0 0 384 512" :fill="color"
xmlns="http://www.w3.org/2000/svg"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path
d="M169.4 470.6c12.5 12.5 32.8 12.5 45.3 0l160-160c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L224 370.8 224 64c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 306.7L54.6 265.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l160 160z" />
</svg>
</template>

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "DownArrow",
props: {
color: {
type: String,
default: "#737373"
}
}
});
</script>
Loading

0 comments on commit 43bf696

Please sign in to comment.