Skip to content

Commit

Permalink
Left panel timeline management (#2999)
Browse files Browse the repository at this point in the history
Introducing a timeline management entry in the left panel and allowing to hide the timelines in the explore view.

---------

Co-authored-by: Janosch <[email protected]>
  • Loading branch information
Annoraaq and jkppr authored Dec 5, 2023
1 parent e36a4ee commit 33bd484
Show file tree
Hide file tree
Showing 11 changed files with 1,102 additions and 629 deletions.
2 changes: 1 addition & 1 deletion timesketch/frontend-ng/src/assets/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ html {
width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis
text-overflow: ellipsis;
}

.theme--dark.v-navigation-drawer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,7 @@ export default {
this.eventList.objects = response.data.objects
this.eventList.meta = response.data.meta
this.searchInProgress = false
EventBus.$emit('updateCountPerTimeline', response.data.meta.count_per_timeline)
this.$emit('countPerTimeline', response.data.meta.count_per_timeline)
this.$emit('countPerIndex', response.data.meta.count_per_index)

Expand Down
599 changes: 65 additions & 534 deletions timesketch/frontend-ng/src/components/Explore/TimelineChip.vue

Large diffs are not rendered by default.

587 changes: 587 additions & 0 deletions timesketch/frontend-ng/src/components/Explore/TimelineComponent.vue

Large diffs are not rendered by default.

59 changes: 20 additions & 39 deletions timesketch/frontend-ng/src/components/Explore/TimelinePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
<span>
<ts-timeline-chip
v-for="timeline in allTimelines"
class="mr-2 mb-3 timeline-chip"
:key="timeline.id + timeline.name"
:timeline="timeline"
:is-selected="isSelected(timeline)"
Expand All @@ -40,16 +41,6 @@ limitations under the License.
<span v-else> {{ sketch.timelines.length - 20 }} more.. </span>
</v-btn>
<br />
<span v-if="sketch.timelines.length > 5">
<v-btn small text rounded color="primary" @click="enableAllTimelines()">
<v-icon left small>mdi-checkbox-outline</v-icon>
<span>Select all</span>
</v-btn>
<v-btn small text rounded color="primary" @click="disableAllTimelines()">
<v-icon left small>mdi-minus-box-outline</v-icon>
<span>Unselect all</span>
</v-btn>
</span>
</span>
</template>

Expand All @@ -58,6 +49,8 @@ import EventBus from '../../main'
import TsTimelineChip from './TimelineChip'
import ApiClient from '../../utils/RestApiClient'

import _ from 'lodash'

export default {
components: { TsTimelineChip },
props: ['currentQueryFilter', 'countPerIndex', 'countPerTimeline'],
Expand Down Expand Up @@ -91,13 +84,12 @@ export default {
return {
isDarkTheme: false,
isLoading: false,
selectedTimelines: [],
showAll: false,
}
},
methods: {
isSelected(timeline) {
return this.selectedTimelines.map((x) => x.id).includes(timeline.id)
return this.$store.state.enabledTimelines.includes(timeline.id)
},
getCount(timeline) {
let count = 0
Expand Down Expand Up @@ -150,35 +142,18 @@ export default {
this.isLoading = false
})
},
enableAllTimelines() {
this.selectedTimelines = this.activeTimelines
this.$emit('updateSelectedTimelines', this.selectedTimelines)
},
disableAllTimelines() {
this.selectedTimelines = []
this.$emit('updateSelectedTimelines', this.selectedTimelines)
},
disableAllOtherTimelines(timeline) {
this.selectedTimelines = [timeline]
this.$emit('updateSelectedTimelines', this.selectedTimelines)
this.$store.dispatch('updateEnabledTimelines', [timeline.id])
},
toggleTimeline(timeline) {
let newArray = this.selectedTimelines.slice()
let timelineIdx = newArray.map((x) => x.id).indexOf(timeline.id)
if (timelineIdx === -1) {
newArray.push(timeline)
} else {
newArray.splice(timelineIdx, 1)
}
this.selectedTimelines = newArray
this.$emit('updateSelectedTimelines', this.selectedTimelines)
this.$store.dispatch('toggleEnabledTimeline', timeline.id)
},
toggleTheme() {
this.isDarkTheme = !this.isDarkTheme
},
syncSelectedTimelines() {
if (this.currentQueryFilter.indices.includes('_all')) {
this.selectedTimelines = this.activeTimelines
this.updateEnabledTimelinesIfChanged(this.activeTimelines.map((tl) => tl.id))
return
}
let newArray = []
Expand All @@ -195,17 +170,16 @@ export default {
newArray.push(timeline)
}
})
this.selectedTimelines = newArray
this.updateEnabledTimelinesIfChanged(newArray.map((tl) => tl.id))
},
updateEnabledTimelinesIfChanged(newTimelineIds) {
if (!_.isEqual(newTimelineIds, this.$store.state.enabledTimelines)) {
this.$store.dispatch('updateEnabledTimelines', newTimelineIds)
}
},
},
created() {
EventBus.$on('isDarkTheme', this.toggleTheme)

if (this.currentQueryFilter.indices.includes('_all')) {
this.selectedTimelines = this.activeTimelines
} else {
this.syncSelectedTimelines()
}
},
watch: {
'currentQueryFilter.indices'(val) {
Expand All @@ -215,3 +189,10 @@ export default {
},
}
</script>

<!-- CSS scoped to this component only -->
<style scoped lang="scss">
.timeline-chip {
display: inline-block;
}
</style>
255 changes: 255 additions & 0 deletions timesketch/frontend-ng/src/components/LeftPanel/TimelinesTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
<!--
Copyright 2023 Google Inc. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

<template>
<div class="content">
<div
class="pa-4"
:style="'cursor: pointer'"
@click="expanded = !expanded"
:class="$vuetify.theme.dark ? 'dark-hover' : 'light-hover'"
>
<span> <v-icon left>mdi-timeline-clock-outline</v-icon> Timelines </span>
<ts-upload-timeline-form v-if="expanded">
<template v-slot="slotProps">
<v-btn
v-if="expanded || allTimelines.length === 0"
icon
text
class="float-right mt-n1 mr-n1"
v-bind="slotProps.attrs"
v-on="slotProps.on"
@click.stop=""
>
<v-icon title="Add timeline">mdi-plus</v-icon>
</v-btn>
</template>
</ts-upload-timeline-form>
<span v-else class="float-right" style="margin-right: 10px">
<small class="ml-3"
><strong>{{ allTimelines.length }}</strong></small
>
</span>
</div>
<v-expand-transition>
<div v-show="expanded">
<v-text-field
v-if="allTimelines.length >= paginationThreshold"
class="ma-3"
v-model="search"
label="Filter timelines"
single-line
clearable
hide-details
outlined
dense
prepend-inner-icon="mdi-magnify"
></v-text-field>
<v-data-table
class="data-table"
:hide-default-footer="allTimelines.length <= paginationThreshold"
:hide-default-header="true"
v-model="selected"
:items="allTimelines"
:headers="headers"
item-key="id"
dense
disable-sort
:search="search"
>
<template v-slot:item.name="{ item }">
<ts-timeline-component
class="mb-1 mt-1"
:key="item.id + item.name"
:is-selected="isEnabled(item)"
@toggle="toggleTimeline"
@disableAllOtherTimelines="disableAllOtherTimelines"
:timeline="item"
>
<template v-slot:processing="slotProps">
<div class="chip-content" :style="timelineStyle(slotProps.timelineStatus, isEnabled(item))">
<span class="timeline-name-ellipsis">{{ item.name }}</span>
<span class="right">
<span v-if="slotProps.timelineStatus === 'processing'" class="ml-3 mr-3">
<v-progress-circular small indeterminate color="grey" :size="17" :width="2"></v-progress-circular>
</span>
</span>
</div>
</template>
<template v-slot:processed="slotProps">
<div class="chip-content" :style="timelineStyle(slotProps.timelineStatus, isEnabled(item))">
<v-icon
v-if="slotProps.timelineFailed"
title="Import failed; click for details"
@click="slotProps.events.openDialog"
left
color="red"
size="x-large"
class="ml-n2"
>
mdi-alert-circle-outline
</v-icon>
<v-icon
v-if="!slotProps.timelineFailed"
left
:color="slotProps.timelineChipColor"
size="26"
class="ml-n2"
>
mdi-circle
</v-icon>

<v-tooltip bottom :disabled="item.name.length < 40" open-delay="200">
<template v-slot:activator="{ on: onTooltip }">
<span v-on="onTooltip" class="timeline-name-ellipsis" style="cursor: default;">{{ item.name }}</span>
</template>
<span>{{ item.name }}</span>
</v-tooltip>

<span class="right">
<span v-if="!slotProps.timelineFailed" class="events-count mr-1" x-small>
{{ getCount(item) | compactNumber }}
</span>
<v-btn
v-if="!slotProps.timelineFailed"
class="ma-1"
x-small
icon
@click="slotProps.events.toggleTimeline"
>
<v-icon v-if="isEnabled(item)"> mdi-eye </v-icon>
<v-icon v-else> mdi-eye-off </v-icon>
</v-btn>
<v-btn class="ma-1" x-small icon v-on="slotProps.events.menuOn">
<v-icon> mdi-dots-vertical </v-icon>
</v-btn>
</span>
</div>
</template>
</ts-timeline-component>
</template>
</v-data-table>
</div>
</v-expand-transition>
<v-divider></v-divider>
</div>
</template>

<script>
import EventBus from '../../main'

import TsUploadTimelineForm from '../UploadForm'
import TsTimelineComponent from '../Explore/TimelineComponent'
export default {
props: [],
components: {
TsUploadTimelineForm,
TsTimelineComponent,
},
computed: {
sketch() {
return this.$store.state.sketch
},
allTimelines() {
// Sort alphabetically based on timeline name.
let timelines = [...this.sketch.timelines]
timelines.sort(function (a, b) {
return a.name.localeCompare(b.name)
})
return timelines
},
activeTimelines() {
// Sort alphabetically based on timeline name.
let timelines = [...this.sketch.active_timelines]
return timelines.sort(function (a, b) {
return a.name.localeCompare(b.name)
})
},
},
methods: {
isEnabled(timeline) {
return this.$store.state.enabledTimelines.includes(timeline.id)
},
toggleTimeline(timeline) {
this.$store.dispatch('toggleEnabledTimeline', timeline.id)
},
disableAllOtherTimelines(timeline) {
this.$store.dispatch('updateEnabledTimelines', [timeline.id])
},
timelineStyle(timelineStatus, isSelected) {
const greyOut = timelineStatus === 'ready' && !isSelected
return {
opacity: greyOut ? '50%' : '100%',
}
},
updateCountPerTimeline(countPerTimeline) {
this.countPerTimeline = countPerTimeline
},
getCount(timeline) {
let count = 0
if (this.countPerTimeline) {
count = this.countPerTimeline[timeline.id]
if (typeof count === 'number') {
return count
}
}
return count
},
},
data: function () {
return {
countPerTimeline: {},
expanded: false,
selected: [],
search: '',
headers: [{ value: 'name' }],
paginationThreshold: 10,
}
},
created() {
this.$store.dispatch(
'updateEnabledTimelines',
this.activeTimelines.map((tl) => tl.id)
)
},
mounted() {
EventBus.$on('updateCountPerTimeline', this.updateCountPerTimeline)
},
beforeDestroy() {
EventBus.$off('updateCountPerTimeline')
},
}
</script>

<!-- CSS scoped to this component only -->
<style scoped lang="scss">
.chip-content {
flex: 1;
margin: 0;
padding: 0 10px;
display: flex;
align-items: center;
}

.timeline-name.disabled {
text-decoration: line-through;
}
.right {
align-items: center;
display: flex;
margin-left: auto;
}
</style>
Loading

0 comments on commit 33bd484

Please sign in to comment.