Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ApexChart based visualizations #3040

Merged
merged 57 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
8c5e881
Initial skeleton UI
Aug 16, 2023
e9d363e
Merge branch 'master' into agg_v2
Dec 21, 2023
b6fa8f9
Add WIP Visualization VueJS components
Jan 9, 2024
4fd57e3
Updates
Jan 10, 2024
2853a0e
Updates
sydp Feb 15, 2024
f883354
Merge branch 'master' into agg_v2_2024
sydp Feb 16, 2024
faa990c
Fixes
sydp Feb 16, 2024
346a554
Fixes
sydp Feb 16, 2024
a5e2b19
Updates
sydp Feb 17, 2024
2cd2611
Formatting and remove number chart
sydp Feb 17, 2024
b8827d8
Add icon to left menu
sydp Feb 18, 2024
813ffb0
Fixes
sydp Feb 18, 2024
eb08484
Updates to API and add daterange chip filtering to charts
sydp Feb 18, 2024
f5cea3f
Lint fixes
sydp Feb 18, 2024
3b7e997
whitespace and more eslint
sydp Feb 18, 2024
3ab98a3
black fix
sydp Feb 18, 2024
78a7038
pylint appeasement
sydp Feb 18, 2024
a59aaf1
Revert changes to Analyze.vue and Sketch.vue
sydp Feb 18, 2024
7455d5c
Merge branch 'master' into agg_v2_2024
sydp Feb 18, 2024
cdb1086
Merge branch 'master' into agg_v2_2024
jkppr Apr 4, 2024
12a581a
Simplify layout
sydp Apr 21, 2024
937f8bc
Merge branch 'master' into agg_v2_2024
sydp Apr 21, 2024
b4130a2
Add switch to show chart options
sydp Apr 21, 2024
28cc4c8
Remove AggregationFiltersPanel
sydp Apr 21, 2024
02b12dd
Remove Saved and Recent Search components
sydp Apr 21, 2024
c6ccf6a
Merge branch 'master' into agg_v2_2024
jkppr Apr 26, 2024
c60fd3a
Merge branch 'master' into agg_v2_2024
jkppr May 14, 2024
a3349ea
Merge branch 'master' into agg_v2_2024
jkppr May 17, 2024
90fa1bc
Update timesketch/frontend-ng/src/components/LeftPanel/Visualizations…
sydp May 20, 2024
64eb06e
Update timesketch/frontend-ng/src/components/LeftPanel/Visualizations…
sydp May 20, 2024
06de739
Update timesketch/frontend-ng/src/views/Visualization.vue
sydp May 20, 2024
65633a5
Changes per review
sydp May 20, 2024
8b60de9
Add aggregator_class to meta for filtering out legacy aggregations
sydp May 20, 2024
ddb2ce6
Filter visualization count in left panel
sydp May 20, 2024
6ef5259
black formatting
sydp May 20, 2024
dc0ed62
Update left panel styling to truncate long titles
sydp May 20, 2024
e356e6a
Added tooltip
sydp May 20, 2024
59e459a
Disable tooltip on short title name
sydp May 20, 2024
59a66ca
Update timesketch/frontend-ng/src/components/LeftPanel/Visualizations…
sydp May 21, 2024
7d05c52
Update timesketch/frontend-ng/src/components/Visualization/SavedVisua…
sydp May 21, 2024
55d410e
Add outlined to ChartCard.vue
sydp May 21, 2024
518f834
Fix mutating prop error
sydp May 21, 2024
4db0548
Fix suggested commit
sydp May 21, 2024
44b8b3c
Remove vcard per review
sydp May 22, 2024
4118dc0
Update selectedMaxItems control to v-text-field
sydp May 22, 2024
7309a30
Update timesketch/frontend-ng/src/components/Visualization/Aggregatio…
sydp May 23, 2024
97b7730
Update metric type
sydp May 23, 2024
f206a40
Fix prop error with maxItems
sydp May 23, 2024
966ff55
Updates per suggestion
sydp May 23, 2024
96da6e9
Update chart card
sydp May 23, 2024
7bea35f
Merge branch 'master' into agg_v2_2024
jkppr Jul 8, 2024
7472b20
Adjusted the UI following latest UX mocks
jkppr Jul 12, 2024
3df0a2d
Merge pull request #1 from jkppr/agg_v2_jkpr_ui
sydp Jul 15, 2024
7b12a72
Merge branch 'master' into agg_v2_2024
sydp Jul 15, 2024
ece53c0
Updates
sydp Jul 16, 2024
1b9472b
Updates
sydp Jul 16, 2024
5833674
Add tooltips
sydp Jul 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 35 additions & 15 deletions timesketch/api/v1/resources/aggregation.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from timesketch.api.v1 import utils
from timesketch.lib import forms
from timesketch.lib import utils as lib_utils
from timesketch.lib.aggregators import apex
from timesketch.lib.definitions import HTTP_STATUS_CODE_OK
from timesketch.lib.definitions import HTTP_STATUS_CODE_CREATED
from timesketch.lib.definitions import HTTP_STATUS_CODE_BAD_REQUEST
Expand Down Expand Up @@ -69,6 +70,12 @@ def get(self, sketch_id, aggregation_id): # pylint: disable=unused-argument
)
aggregation = Aggregation.get_by_id(aggregation_id)

# Check that the aggregation exists
if not aggregation:
abort(
HTTP_STATUS_CODE_NOT_FOUND,
"The aggregation ID ({0:d}) does not exist.".format(aggregation_id),
)
# Check that this aggregation belongs to the sketch
if aggregation.sketch_id != sketch.id:
abort(
Expand Down Expand Up @@ -468,17 +475,20 @@ def post(self, sketch_id):
aggregator_name = form.aggregator_name.data

if aggregator_name:
if isinstance(form.aggregator_parameters.data, dict):
aggregator_parameters = form.aggregator_parameters.data
else:
aggregator_parameters = json.loads(form.aggregator_parameters.data)

agg_class = aggregator_manager.AggregatorManager.get_aggregator(
aggregator_name
)
if not agg_class:
return {}
if not aggregator_parameters:
abort(
HTTP_STATUS_CODE_NOT_FOUND,
f"Aggregator {aggregator_name} not found",
)

if form.aggregator_parameters.data:
aggregator_parameters = form.aggregator_parameters.data
if not isinstance(aggregator_parameters, dict):
aggregator_parameters = json.loads(aggregator_parameters)
else:
aggregator_parameters = {}

indices = aggregator_parameters.pop("index", sketch_indices)
Expand All @@ -490,12 +500,10 @@ def post(self, sketch_id):
aggregator = agg_class(
sketch_id=sketch_id, indices=indices, timeline_ids=timeline_ids
)
aggregator_description = aggregator.describe

# legacy chart settings
chart_type = aggregator_parameters.pop("supported_charts", None)
chart_color = aggregator_parameters.pop("chart_color", "")
chart_title = aggregator_parameters.pop(
"chart_title", aggregator.chart_title
)

time_before = time.time()
try:
Expand All @@ -515,24 +523,36 @@ def post(self, sketch_id):
)
time_after = time.time()

aggregator_description = aggregator.describe

buckets = result_obj.to_dict()
buckets["buckets"] = buckets.pop("values")
if "labels" in buckets:
buckets["labels"] = buckets.pop("labels")
if "chart_options" in buckets:
buckets["chart_options"] = buckets.pop("chart_options")

result = {"aggregation_result": {aggregator_name: buckets}}
meta = {
"method": "aggregator_run",
"aggregator_class": (
"apex" if isinstance(aggregator, apex.ApexAggregation) else "legacy"
),
"chart_type": chart_type,
"name": aggregator_description.get("name"),
"description": aggregator_description.get("description"),
"es_time": time_after - time_before,
}

if chart_type:
meta["vega_spec"] = result_obj.to_chart(
chart_color = aggregator_parameters.pop("chart_color", "")
chart_title = aggregator_parameters.pop("chart_title", None)
chart_spec = result_obj.to_chart(
chart_name=chart_type, chart_title=chart_title, color=chart_color
)
meta["vega_chart_title"] = chart_title
if chart_spec:
meta["vega_spec"] = chart_spec
if not chart_title:
chart_title = aggregator.chart_title
meta["vega_chart_title"] = chart_title

elif aggregation_dsl:
# pylint: disable=unexpected-keyword-arg
Expand Down
271 changes: 271 additions & 0 deletions timesketch/frontend-ng/src/components/LeftPanel/Visualizations.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
<!--
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
v-if="iconOnly"
key="iconOnly"
class="pa-4"
style="cursor: pointer"
@click="
$emit('toggleDrawer')
expanded = true
"
>
<v-icon left>
mdi-chart-bar
</v-icon>
<div style="height: 1px">
</div>
</div>
<div
v-else
key="iconOnly"
>
<div
:style="!(savedVisualizations && savedVisualizations.length) ? '' : 'cursor: pointer'"
class="pa-4"
@click="expanded = !expanded"
:class="$vuetify.theme.dark ? 'dark-hover' : 'light-hover'"
>
<span>
<v-icon left>
mdi-chart-bar
</v-icon>
Visualizations
</span>

<v-btn
v-if="expanded || !(savedVisualizations && savedVisualizations.length)"
icon
text
class="float-right mt-n1 mr-n1"
:to="{ name: 'VisualizationNew' }"
@click.stop=""
>
<v-icon title="Create a new visualization">
mdi-plus
</v-icon>
</v-btn>
<span
v-if="!expanded"
class="float-right"
style="margin-right: 10px"
>
<small
v-if="savedVisualizations && savedVisualizations.length"
>
<strong>
{{ visualizationCount }}
</strong>
</small>
</span>
</div>
<v-expand-transition>
<div v-show="expanded && savedVisualizations.length">
<div
v-for="(savedVisualization, key) in savedVisualizations"
:key="key"

style="cursor: pointer; font-size: 0.9em; text-decoration: none"

>
<v-row
no-gutters
class="pa-2 pl-5"
:class="$vuetify.theme.dark ? 'dark-hover' : 'light-hover'"
>
<v-col
:class="$vuetify.theme.dark ? 'dark-font' : 'light-font'"
@click="navigateToSavedVisualization(savedVisualization.id)"
>

<span class="d-inline-block text-truncate" style="max-width: 250px">
<v-icon left small>
{{ getIcon(savedVisualization.chart_type) }}
</v-icon>
<!-- {{ savedVisualization.name }} -->
<v-tooltip bottom :disabled="savedVisualization.name && savedVisualization.name.length < 34">
<template v-slot:activator="{ on, attrs }">
<span
v-bind="attrs"
v-on="on"
>{{ savedVisualization.name }}</span>
</template>
<span>{{ savedVisualization.name }}</span>
</v-tooltip>
</span>

</v-col>
<v-col cols="auto">
<v-menu offset-y>
<template v-slot:activator="{ on, attrs }">
<v-btn
small
icon
v-bind="attrs"
v-on="on"
class="mr-1"
>
<v-icon
title="Actions"
small
>
mdi-dots-vertical
</v-icon>
</v-btn>
</template>
<v-list dense class="mx-auto">
<v-list-item style="cursor: pointer" @click="copyVisualizationIdToClipboard(savedVisualization.id)">
<v-list-item-icon>
<v-icon small>mdi-identifier</v-icon>
</v-list-item-icon>
<v-list-item-title>Copy visualization ID</v-list-item-title>
</v-list-item>
<v-list-item style="cursor: pointer" @click="copyVisualizationUrlToClipboard(savedVisualization.id)">
<v-list-item-icon>
<v-icon small>mdi-link-variant</v-icon>
</v-list-item-icon>
<v-list-item-title>Copy link to this visualization</v-list-item-title>
</v-list-item>
<v-list-item style="cursor: pointer" @click="deleteVisualization(savedVisualization.id)">
<v-list-item-icon>
<v-icon small>mdi-trash-can</v-icon>
</v-list-item-icon>
<v-list-item-title>Delete</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-col>
</v-row>
</div>
</div>
</v-expand-transition>
<v-divider></v-divider>
</div>
</template>

<script>
import ApiClient from '../../utils/RestApiClient'

export default {
props: {
iconOnly: {
type: Boolean,
},
},
data: function () {
return {
expanded: false,
}
},
methods: {
copyVisualizationIdToClipboard(savedVisualizationId) {
try {
navigator.clipboard.writeText(savedVisualizationId)
this.infoSnackBar('Saved Visualization ID copied to clipboard')
} catch (error) {
this.errorSnackBar('Failed to load Saved Visualization ID into the clipboard!')
console.error(error)
}
},
copyVisualizationUrlToClipboard(savedVisualizationId) {
try {
let url = window.location.origin + '/sketch/' + this.sketch.id + '/visualization/view/' + savedVisualizationId
navigator.clipboard.writeText(url)
this.infoSnackBar('Saved Visualization URL copied to clipboard')
} catch (error) {
this.errorSnackBar('Failed to load Saved Visualization URL into the clipboard!')
console.error(error)
}
},
deleteVisualization(savedVisualizationId) {
if (confirm('Delete Saved Visualization?')) {
ApiClient.deleteAggregationById(this.sketch.id, savedVisualizationId)
.then((response) => {
this.$store.dispatch('updateSavedVisualizationList', this.sketch.id)
this.infoSnackBar('Saved Visualization has been deleted')
let params = {
name: 'VisualizationView',
params: {
aggregationId: savedVisualizationId
}
}
let currentPath = this.$route.fullPath
let deletedPath = this.$router.resolve(params).route.fullPath

if (currentPath === deletedPath) {
this.$router.push({ name: 'VisualizationNew', })
}
})
.catch((e) => {
this.errorSnackBar('Failed to delete Saved Visualization!')
console.error(e)
})
}
},
getIcon(chartType) {
return {
'bar': 'mdi-poll mdi-rotate-90',
'column': 'mdi-chart-bar',
'line': 'mdi-chart-line',
'table': 'mdi-table',
'heatmap': 'mdi-blur-linear',
'donut': 'mdi-chart-donut',

}[chartType]
},
navigateToSavedVisualization(savedVisualizationId) {
let params = {
name: 'VisualizationView',
params: { aggregationId: savedVisualizationId }
}
let nextPath = this.$router.resolve(params).route.fullPath
let currentPath = this.$route.fullPath
if (nextPath !== currentPath) {
this.$router.push(params)
}
}
},
computed: {
savedVisualizations() {
if (!this.$store.state.savedVisualizations) {
return []
}
return this.$store.state.savedVisualizations.filter(
(e) => JSON.parse(e.parameters)['aggregator_class'] === 'apex'
)
},
visualizationCount() {
if (!this.$store.state.savedVisualizations) {
return 0
}
return this.$store.state.savedVisualizations.filter(
(e) => JSON.parse(e.parameters)['aggregator_class'] === 'apex'
).length
},
sketch() {
return this.$store.state.sketch
},
meta() {
return this.$store.state.meta
},
},
mounted() {
this.$store.dispatch('updateSavedVisualizationList', this.sketch.id)
},

}
</script>
Loading
Loading