Skip to content

Commit

Permalink
allow column manipulation of event table
Browse files Browse the repository at this point in the history
  • Loading branch information
jertel committed Jan 9, 2024
1 parent fc32c29 commit 95e37bb
Show file tree
Hide file tree
Showing 7 changed files with 323 additions and 28 deletions.
4 changes: 4 additions & 0 deletions html/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@
max-width: 300px;
}

.tiny-icon {
font-size: 14px !important;
}

.stale {
opacity: 0.3;
}
Expand Down
57 changes: 42 additions & 15 deletions html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ <h4 v-if="loaded">{{ i18n.eventTotal }} {{ totalEvents.toLocaleString() }}</h4>
<v-icon>fa-expand</v-icon>
</router-link>
<v-btn icon class="mx-1" @click="removeGroupBy(groupIdx, -1)" :title="i18n.remove">
<v-icon>fa-trash</v-icon>
<v-icon class="tiny-icon">fa-circle-xmark</v-icon>
</v-btn>
<pie-chart v-if="group.chart_type == 'pie'" :id="'group-' + groupIdx" :chartdata="group.chart_data" :options="group.chart_options"/>
<bar-chart v-if="group.chart_type == 'bar'" :id="'group-' + groupIdx" :chartdata="group.chart_data" :options="group.chart_options"/>
Expand All @@ -554,7 +554,7 @@ <h4 v-if="loaded">{{ i18n.eventTotal }} {{ totalEvents.toLocaleString() }}</h4>
<v-icon>fa-expand</v-icon>
</router-link>
<v-btn v-if="header.value != 'count' && header.value != ''" icon x-small @click.stop="removeGroupBy(groupIdx,headerIdx - getGroupByFieldStartIndex())" :title="i18n.remove">
<v-icon>fa-trash</v-icon>
<v-icon class="tiny-icon">fa-circle-xmark</v-icon>
</v-btn>
</span>
</template>
Expand Down Expand Up @@ -630,6 +630,26 @@ <h4 v-if="loaded">{{ i18n.eventTotal }} {{ totalEvents.toLocaleString() }}</h4>
</v-row>
</v-container>
</template>
<template v-for="(header, headerIdx) in eventHeaders" v-slot:["header." + header.value]="{ props }">
<span :id="'col_' + header.value">
<span v-if="header.value != ''" class="table-header-actions">
<v-btn :id="'col_move_left_' + header.value" icon x-small @click.stop="moveColumnHeader(header.value, true)" :title="i18n.moveLeft">
<v-icon class="tiny-icon">fa-caret-left</v-icon>
</v-btn>
</span>
<span :id="'col_label_' + header.value">
{{ header.text }}
</span>
<span v-if="header.value != ''" class="table-header-actions">
<v-btn :id="'col_move_right_' + header.value" icon x-small @click.stop="moveColumnHeader(header.value, false)" :title="i18n.moveRight">
<v-icon class="tiny-icon">fa-caret-right</v-icon>
</v-btn>
<v-btn :id="'col_del_' + header.value" icon x-small @click.stop="removeColumnHeader(header.value)" :title="i18n.remove">
<v-icon class="tiny-icon">fa-circle-xmark</v-icon>
</v-btn>
</span>
</span>
</template>
<template v-slot:item="{ item, index }">
<tr>
<td>
Expand Down Expand Up @@ -667,9 +687,12 @@ <h4 v-if="loaded">{{ i18n.eventTotal }} {{ totalEvents.toLocaleString() }}</h4>
<tr class="pt-3">
<td class="expansion">
<div class="d-inline-block semi-transparent">
<v-btn :disabled="!canQuery(item.key)" icon x-small :to="buildGroupByRoute(item.key)">
<v-btn :id="'groupby_' + item.key" :disabled="!canQuery(item.key)" icon x-small :to="buildGroupByRoute(item.key)">
<v-icon :alt="i18n.groupInclude" :title="i18n.groupIncludeHelp">fa-layer-group</v-icon>
</v-btn>
<v-btn :id="'col_toggle_' + item.key" icon x-small @click="toggleColumnHeader(item.key)">
<v-icon :alt="i18n.addColumnHeader" :color="isColumnHeader(item.key) ? 'info' : ''" :title="i18n.addColumnHeaderHelp">fa-table-columns</v-icon>
</v-btn>
</div>
<div class="d-inline-block expansion" v-text="item.key"></div>
</td>
Expand Down Expand Up @@ -1723,9 +1746,9 @@ <h2>{{ i18n.description }}</h2>
<div class="ml-1 d-flex mt-1 mt-sm-0">
<v-btn text small icon color="primary" v-if="!isEdit(`comment-${index}`)" @click="startEdit('comment-desc-input', comment.description, `comment-${index}`, 'description', modifyAssociation, ['comments', comment], true)"
@keypress.space.prevent="startEdit('comment-desc-input', comment.description, `comment-${index}`, 'description', modifyAssociation, ['comments', comment], true)">
<v-icon style="font-size: 14px !important;">fa-edit</v-icon>
<v-icon class="tiny-icon">fa-edit</v-icon>
</v-btn>
<v-btn text small color="red" @click="deleteAssociation('comments', comment)" icon><v-icon style="font-size: 14px !important;">fa-trash</v-icon></v-btn>
<v-btn text small color="red" @click="deleteAssociation('comments', comment)" icon><v-icon class="tiny-icon">fa-circle-xmark</v-icon></v-btn>
</div>
</div>
</div>
Expand Down Expand Up @@ -1997,7 +2020,7 @@ <h3 class="text--primary">{{ i18n.attachmentAdd }}</h3>
{{ i18n.edited }}
</div>
<div class="ml-1 d-flex">
<v-btn text small color="red" @click="deleteAssociation('attachments', props.item)" icon><v-icon style="font-size: 14px !important;">fa-trash</v-icon></v-btn>
<v-btn text small color="red" @click="deleteAssociation('attachments', props.item)" icon><v-icon class="tiny-icon">fa-circle-xmark</v-icon></v-btn>
</div>
</div>
</td>
Expand Down Expand Up @@ -2285,7 +2308,7 @@ <h3 class="text--primary">{{ i18n.evidenceAdd }}</h3>
{{ job.completeTime | formatDateTime }}
</div>
<div class="ml-1 d-flex">
<v-btn text small color="red" @click="deleteAnalyzeJob(job)" icon><v-icon style="font-size: 14px !important;">fa-trash</v-icon></v-btn>
<v-btn text small color="red" @click="deleteAnalyzeJob(job)" icon><v-icon class="tiny-icon">fa-circle-xmark</v-icon></v-btn>
</div>
<v-spacer/>
</div>
Expand Down Expand Up @@ -2316,7 +2339,7 @@ <h3 class="text--primary">{{ i18n.evidenceAdd }}</h3>
{{ i18n.edited }}
</div>
<div class="ml-1 d-flex">
<v-btn text small color="red" @click="deleteAssociation('evidence', props.item)" icon><v-icon style="font-size: 14px !important;">fa-trash</v-icon></v-btn>
<v-btn text small color="red" @click="deleteAssociation('evidence', props.item)" icon><v-icon class="tiny-icon">fa-circle-xmark</v-icon></v-btn>
</div>
</div>
</td>
Expand Down Expand Up @@ -2400,7 +2423,7 @@ <h3 class="text--primary">{{ i18n.evidenceAdd }}</h3>
{{ props.item.createTime | formatDateTime }}
</div>
<div class="ml-1 d-flex">
<v-btn text small color="red" @click="deleteAssociation('events', props.item)" icon><v-icon style="font-size: 14px !important;">fa-trash</v-icon></v-btn>
<v-btn text small color="red" @click="deleteAssociation('events', props.item)" icon><v-icon class="tiny-icon">fa-circle-xmark</v-icon></v-btn>
</div>
</div>
</td>
Expand Down Expand Up @@ -2458,7 +2481,7 @@ <h3 class="text--primary">{{ i18n.evidenceAdd }}</h3>
</td>
<td class="associated">
<v-icon v-if="props.item.operation == i18n.create">fa-plus</v-icon>
<v-icon v-if="props.item.operation == i18n.delete">fa-trash</v-icon>
<v-icon v-if="props.item.operation == i18n.delete">fa-circle-xmark</v-icon>
<v-icon v-if="props.item.operation == i18n.update">fa-pencil-alt</v-icon>
<span class="rounded ml-2">{{ props.item.operation }}</span>
</td>
Expand Down Expand Up @@ -3097,8 +3120,12 @@ <h4 v-if="metricsEnabled">{{ i18n.gridEps }} {{ gridEps | formatCount }}</h4>
</div>
<div class="text-no-wrap">
<span class="filter label">{{ i18n.osUptime }}:</span>
<span class="filter value" id="node_uptime" :class="areMetricsCurrent(item) ? '' : 'stale'">{{ item.osUptimeSeconds | formatDuration }}</span>
<span v-if="item.osNeedsRestart" id="node_restart-required-note" class="warning--text" :class="areMetricsCurrent(item) ? '' : 'stale'">{{ i18n.restartRequired }}</span>
<span class="filter value" id="node_uptime" :class="areMetricsCurrent(item) ? '' : 'stale'">
{{ item.osUptimeSeconds | formatDuration }}
<span v-if="item.osNeedsRestart" id="node_restart-required-note" class="warning--text" :class="areMetricsCurrent(item) ? '' : 'stale'">
{{ i18n.restartRequired }}
</span>
</span>
</div>
<div v-if="item.metricsEnabled" class="text-no-wrap">
<span class="filter label">{{ i18n.lastHighstate }}:</span>
Expand Down Expand Up @@ -3552,7 +3579,7 @@ <h2 v-text="i18n.settingsTitle"></h2>
<div :id="'settingsWebauthnExistingKey_' + key.id" class="ml-2">
{{ key.name }}
<v-btn name="webauthn_remove" icon :value="key.value" :id="'settingsWebauthnDelete_' + key.id" type="submit" color="primary" >
<v-icon>fa-trash</v-icon>
<v-icon>fa-circle-xmark</v-icon>
</v-btn>
</div>
</v-form>
Expand Down Expand Up @@ -3806,7 +3833,7 @@ <h4 v-if="!$root.loading">{{ i18n.settingsCustomized }} {{ settingsCustomized }}
<v-col>
<div class="mb-n1 text-right">
<v-btn :id="'setting-node-reset-' + item[0]" icon @click="remove(selected, item[0])" :title="i18n.settingRemoveHelp" :disabled="isPendingSave(selected, item[0])" >
<v-icon color="warning">fa-trash</v-icon>
<v-icon color="warning">fa-circle-xmark</v-icon>
</v-btn>
<v-btn :id="'setting-node-save-' + item[0]" icon @click="save(selected, item[0])" :disabled="!isPendingSave(selected, item[0])" :title="i18n.settingSaveHelp">
<v-icon color="success">fa-check</v-icon>
Expand Down Expand Up @@ -4032,7 +4059,7 @@ <h2 v-text="i18n.gridMembers"></h2>
</v-btn>
<v-btn id="gridmember-remove" @click.stop="confirmRemove()">
{{ i18n.delete }}
<v-icon right>fa-trash</v-icon>
<v-icon right>fa-circle-xmark</v-icon>
</v-btn>
</v-card-actions>
</v-card>
Expand Down
2 changes: 2 additions & 0 deletions html/js/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ const i18n = {
active: 'Active',
add: 'Add',
addAttachmentHelp: 'Add a new attachment to this case',
addColumnHeader: 'Toggle Table Column',
addColumnHeaderHelp: 'Add or remove this field as a table column',
addCommentHelp: 'Add a new comment to this case',
addInNewTab: 'Add in New Tab',
addObservableHelp: 'Add a new observable to this case',
Expand Down
145 changes: 135 additions & 10 deletions html/js/routes/hunt.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const huntComponent = {
queryGroupBys: [],
queryGroupByOptions: [],
querySortBys: [],
queryTableFields: [],
queryTableOptions: [],
eventFields: {},
dateRange: '',
relativeTimeEnabled: true,
Expand Down Expand Up @@ -137,6 +139,7 @@ const huntComponent = {
addToCaseDialogVisible: false,
mruCases: [],
selectedMruCase: null,
disableRouteLoad: false,
}},
created() {
this.$root.initializeCharts();
Expand Down Expand Up @@ -440,6 +443,8 @@ const huntComponent = {
return true;
},
async loadData() {
if (this.disableRouteLoad) return;

if (!this.parseUrlParameters()) return;

this.$root.startLoading();
Expand Down Expand Up @@ -682,6 +687,8 @@ const huntComponent = {
this.queryFilters = [];
this.queryGroupBys = [];
this.queryGroupByOptions = [];
this.queryTableFields = [];
this.queryTableOptions = [];
this.querySortBys = [];
var route = this;
if (this.query) {
Expand Down Expand Up @@ -751,6 +758,23 @@ const huntComponent = {
route.queryGroupBys.push(fields);
route.queryGroupByOptions.push(options);
}
if (segment.indexOf("table") == 0) {
var fields = [];
var options = [];
segment.split(" ").forEach(function(item, index) {
// Skip empty fields and segment options (they start with a hyphen)
if (item[0] == "-") {
options.push(item.substring(1));
} else if (index > 0 && item.trim().length > 0) {
if (item.split("\"").length % 2 == 1) {
// Will currently skip quoted items with spaces.
fields.push(item);
}
}
});
route.queryTableFields = fields;
route.queryTableOptions = options;
}
if (segment.indexOf("sortby") == 0) {
segment.split(" ").forEach(function(item, index) {
if (index > 0 && item.trim().length > 0) {
Expand Down Expand Up @@ -1054,15 +1078,17 @@ const huntComponent = {
},
constructHeaders(fields) {
var headers = [];
var i18n = this.i18n;
fields.forEach(function(item, index) {
var i18nKey = "field_" + item;
var header = {
text: i18n[i18nKey] ? i18n[i18nKey] : item,
value: item,
};
headers.push(header);
});
if (fields && fields.length > 0) {
var i18n = this.i18n;
fields.forEach(function(item, index) {
var i18nKey = "field_" + item;
var header = {
text: i18n[i18nKey] ? i18n[i18nKey] : item,
value: item,
};
headers.push(header);
});
}
return headers;
},
lookupSocId(data) {
Expand Down Expand Up @@ -1243,9 +1269,108 @@ const huntComponent = {
fields.push(key);
}
}
this.eventHeaders = this.constructHeaders(this.filterVisibleFields(eventModule, eventDataset, fields));
this.populateEventHeaders(this.filterVisibleFields(eventModule, eventDataset, fields));
this.eventData = records;
},
populateEventHeaders(defaultFields) {
var fields = defaultFields;
if (this.queryTableFields.length > 0) {
fields = this.queryTableFields;
}
this.eventHeaders = this.constructHeaders(fields);
},
repopulateEventHeaders() {
this.populateEventHeaders();

// This is a UI interaction so update the query and route to reflect the new table segment
var segments = this.query.split("|");
var newQuery = segments[0];
for (var i = 1; i < segments.length; i++) {
if (segments[i].trim().indexOf("table") == 0) {
continue;
}
newQuery = newQuery.trim() + " | " + segments[i].trim();
}
if (this.queryTableFields.length > 0) {
newQuery = newQuery + " | table " + this.queryTableFields.join(" ");
}

this.updateActiveQuery(newQuery);
},
updateActiveQuery(newQuery) {
var route = this.buildCurrentRoute();
route.query.q = newQuery;

this.disableRouteLoad = true;
this.$router.push(route);
this.query = newQuery;
const thisRoute = this;
setTimeout(function() { thisRoute.disableRouteLoad = false; }, 100);
},
toggleColumnHeader(field) {
if (!this.isColumnHeader(field)) {
this.addColumnHeader(field);
} else {
this.removeColumnHeader(field);
}
},
populateQueryTableFields() {
if (this.queryTableFields.length == 0) {
// Pre-populate with the default field headers already in eventHeaders (from populateEventTable)
for (const idx in this.eventHeaders) {
const field = this.eventHeaders[idx].value;
this.queryTableFields.push(field);
}
}
},
moveColumnHeader(field, left) {
this.populateQueryTableFields();
for (var idx = -1; idx < this.queryTableFields.length; idx++) {
const currentField = this.queryTableFields[idx];
if (field == currentField) {
break;
}
}

if (idx > -1) {
if (left) {
if (idx > 0) {
var tmpFields = this.queryTableFields.slice(0, idx - 1);
tmpFields.push(field);
tmpFields = tmpFields.concat(this.queryTableFields.slice(idx - 1, idx));
this.queryTableFields = tmpFields.concat(this.queryTableFields.slice(idx + 1));
}
} else {
if (idx < this.queryTableFields.length - 1) {
var tmpFields = this.queryTableFields.slice(0, idx);
tmpFields = tmpFields.concat(this.queryTableFields.slice(idx + 1, idx + 2));
tmpFields.push(field);
this.queryTableFields = tmpFields.concat(this.queryTableFields.slice(idx + 2));
}
}
}
this.repopulateEventHeaders(); // no defaults fields will be supplied since we know they aren't going to be used.
},
addColumnHeader(field) {
this.populateQueryTableFields();
this.queryTableFields.push(field);
this.repopulateEventHeaders(); // no defaults fields will be supplied since we know they aren't going to be used.
},
removeColumnHeader(field) {
this.populateQueryTableFields();
this.queryTableFields = this.queryTableFields.filter(item => item != field);

// do not revert back to the predefined headers if this was the last column that was just removed. Otherwise
// users would get frustrated if they're trying to remove all the columns to then add their own.
this.repopulateEventHeaders(); // no defaults fields provided
},
isColumnHeader(field) {
return this.eventHeaders.find((item) => {
if (item.value == field) {
return true;
}
}) != null;
},
displayTable(group, groupIdx) {
group.chart_type = "";
Vue.set(this.groupBys, groupIdx, group);
Expand Down
Loading

0 comments on commit 95e37bb

Please sign in to comment.