refactor: Recorder code to vue3
This commit is contained in:
parent
6bd95516ba
commit
60d9335d38
9 changed files with 384 additions and 369 deletions
|
|
@ -22,7 +22,7 @@ class Recorder {
|
|||
}
|
||||
|
||||
show() {
|
||||
if (!this.view || this.view.$route.name == "recorder-detail") return;
|
||||
this.view.$router.replace({ name: "recorder-detail" });
|
||||
if (!this.route || this.route.name == "RecorderDetail") return;
|
||||
this.router?.replace({ name: "RecorderDetail" });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,15 +5,24 @@
|
|||
<div class="tag-filters-area">
|
||||
<div class="active-tag-filters">
|
||||
<button class="btn btn-default btn-xs add-filter text-muted">
|
||||
{{ __("Add Filter") }}
|
||||
{{ translated_string("Add Filter") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-edit-area"></div>
|
||||
<div class="sort-selector">
|
||||
<div class="dropdown"><a class="text-muted dropdown-toggle small" data-toggle="dropdown"><span class="dropdown-text">{{ columns.filter(c => c.slug == query.sort)[0].label }}</span></a>
|
||||
<div class="dropdown">
|
||||
<a class="text-muted dropdown-toggle small" data-toggle="dropdown">
|
||||
<span class="dropdown-text">
|
||||
{{ columns.filter(c => c.slug == query.sort)[0].label }}
|
||||
</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li v-for="(column, index) in columns.filter(c => c.sortable)" :key="index" @click="query.sort = column.slug"><a class="option">{{ column.label }}</a></li>
|
||||
<li v-for="(column, index) in columns.filter(c => c.sortable)" :key="index" @click="query.sort = column.slug">
|
||||
<a class="option">
|
||||
{{ column.label }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button class="btn btn-default btn-xs btn-order">
|
||||
|
|
@ -71,13 +80,17 @@
|
|||
</div>
|
||||
<div v-if="requests.length == 0" class="no-result text-muted flex justify-center align-center" style="">
|
||||
<div class="msg-box no-border" v-if="status.status == 'Inactive'" >
|
||||
<p><button class="btn btn-primary btn-sm btn-new-doc" @click="start()">{{ __("Start Recording") }}</button></p>
|
||||
<p>{{ __("Recorder is Inactive.") }}</p>
|
||||
<p>{{ __("Start recording or drag & drop a previously exported data file to view it.") }}</p>
|
||||
<p>
|
||||
<button class="btn btn-primary btn-sm btn-new-doc" @click="start()">
|
||||
{{ translated_string("Start Recording") }}
|
||||
</button>
|
||||
</p>
|
||||
<p>{{ translated_string("Recorder is Inactive.") }}</p>
|
||||
<p>{{ translated_string("Start recording or drag & drop a previously exported data file to view it.") }}</p>
|
||||
</div>
|
||||
<div class="msg-box no-border" v-if="status.status == 'Active'" >
|
||||
<p>{{ __("No Requests found") }}</p>
|
||||
<p>{{ __("Go make some noise") }}</p>
|
||||
<p>{{ translated_string("No Requests found") }}</p>
|
||||
<p>{{ translated_string("Go make some noise") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="list-paging-area">
|
||||
|
|
@ -102,181 +115,193 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "RecorderDetail",
|
||||
data() {
|
||||
return {
|
||||
requests: [],
|
||||
columns: [
|
||||
{label: __("Path"), slug: "path"},
|
||||
{label: __("Duration (ms)"), slug: "duration", sortable: true, number: true},
|
||||
{label: __("Time in Queries (ms)"), slug: "time_queries", sortable: true, number: true},
|
||||
{label: __("Queries"), slug: "queries", sortable: true, number: true},
|
||||
{label: __("Method"), slug: "method"},
|
||||
{label: __("Time"), slug: "time", sortable: true},
|
||||
],
|
||||
query: {
|
||||
sort: "duration",
|
||||
order: "desc",
|
||||
filters: {},
|
||||
pagination: {
|
||||
limit: 20,
|
||||
page: 1,
|
||||
total: 0,
|
||||
}
|
||||
},
|
||||
status: {
|
||||
color: "grey",
|
||||
status: "Unknown",
|
||||
},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
let route = frappe.get_route();
|
||||
if (route[2]) {
|
||||
this.$router.push({name: 'request-detail', params: {id: route[2]}});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetch_status();
|
||||
this.refresh();
|
||||
this.$root.page.set_secondary_action(__("Clear"), () => {
|
||||
frappe.set_route("recorder");
|
||||
this.clear();
|
||||
});
|
||||
this.$root.page.add_menu_item("Export data", () => this.export_data());
|
||||
},
|
||||
computed: {
|
||||
pages: function() {
|
||||
const current_page = this.query.pagination.page;
|
||||
const total_pages = this.query.pagination.total;
|
||||
return [{
|
||||
label: __("First"),
|
||||
number: 1,
|
||||
status: (current_page == 1) ? "disabled" : "",
|
||||
},{
|
||||
label: __("Previous"),
|
||||
number: Math.max(current_page - 1, 1),
|
||||
status: (current_page == 1) ? "disabled" : "",
|
||||
}, {
|
||||
label: current_page,
|
||||
number: current_page,
|
||||
status: "btn-info",
|
||||
}, {
|
||||
label: __("Next"),
|
||||
number: Math.min(current_page + 1, total_pages),
|
||||
status: (current_page == total_pages) ? "disabled" : "",
|
||||
}, {
|
||||
label: __("Last"),
|
||||
number: total_pages,
|
||||
status: (current_page == total_pages) ? "disabled" : "",
|
||||
}];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
filtered: function(requests) {
|
||||
requests = requests.slice();
|
||||
const filters = Object.entries(this.query.filters);
|
||||
requests = requests.filter(
|
||||
(r) => filters.map((f) => (r[f[0]] || "").match(f[1])).every(Boolean)
|
||||
);
|
||||
this.query.pagination.total = Math.ceil(requests.length / this.query.pagination.limit);
|
||||
return requests;
|
||||
},
|
||||
paginated: function(requests) {
|
||||
requests = requests.slice();
|
||||
const begin = (this.query.pagination.page - 1) * (this.query.pagination.limit);
|
||||
const end = begin + this.query.pagination.limit;
|
||||
return requests.slice(begin, end);
|
||||
},
|
||||
sorted: function(requests) {
|
||||
requests = requests.slice();
|
||||
const order = (this.query.order == "asc") ? 1 : -1;
|
||||
const sort = this.query.sort;
|
||||
return requests.sort((a,b) => (a[sort] > b[sort]) ? order : -order);
|
||||
},
|
||||
refresh: function() {
|
||||
frappe.call("frappe.recorder.get").then( r => this.requests = r.message);
|
||||
},
|
||||
update: function(message) {
|
||||
this.requests.push(JSON.parse(message));
|
||||
},
|
||||
clear: function() {
|
||||
frappe.call("frappe.recorder.delete").then(r => this.refresh());
|
||||
},
|
||||
start: function() {
|
||||
frappe.call("frappe.recorder.start").then(r => this.fetch_status());
|
||||
},
|
||||
stop: function() {
|
||||
frappe.call("frappe.recorder.stop").then(r => this.fetch_status());
|
||||
},
|
||||
fetch_status: function() {
|
||||
frappe.call("frappe.recorder.status").then(r => this.update_status(r.message));
|
||||
},
|
||||
update_status: function(result) {
|
||||
if(result) {
|
||||
this.status = {status: "Active", color: "green"}
|
||||
} else {
|
||||
this.status = {status: "Inactive", color: "red"}
|
||||
}
|
||||
this.$root.page.set_indicator(this.status.status, this.status.color);
|
||||
if(this.status.status == "Active") {
|
||||
frappe.realtime.on("recorder-dump-event", this.update);
|
||||
} else {
|
||||
frappe.realtime.off("recorder-dump-event", this.update);
|
||||
}
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { translated_string } from './utils'
|
||||
|
||||
this.update_buttons();
|
||||
},
|
||||
update_buttons: function() {
|
||||
if(this.status.status == "Active") {
|
||||
this.$root.page.set_primary_action(__("Stop"), () => {
|
||||
this.stop();
|
||||
});
|
||||
} else {
|
||||
this.$root.page.set_primary_action(__("Start"), () => {
|
||||
this.start();
|
||||
});
|
||||
}
|
||||
},
|
||||
route_to_request_detail(request) {
|
||||
this.$router.push({name: 'request-detail', params: {request, id: request.uuid}});
|
||||
},
|
||||
export_data: function() {
|
||||
if (!this.requests) {
|
||||
return;
|
||||
}
|
||||
frappe.call("frappe.recorder.export_data")
|
||||
.then((r) => {
|
||||
const data = r.message;
|
||||
const filename = `${data[0]['uuid']}..${data[data.length -1]['uuid']}.json`
|
||||
// variables
|
||||
let router = ref(useRouter());
|
||||
let requests = ref([]);
|
||||
let page = frappe.pages['recorder'].page;
|
||||
|
||||
const el = document.createElement('a');
|
||||
el.setAttribute('href', 'data:application/json,' + encodeURIComponent(JSON.stringify(data)));
|
||||
el.setAttribute('download', filename);
|
||||
el.click();
|
||||
});
|
||||
},
|
||||
import_data: function(e) {
|
||||
if (this.requests.length > 0) {
|
||||
// don't replace existing capture
|
||||
return;
|
||||
}
|
||||
const request_file = e.dataTransfer.files[0];
|
||||
let columns = [
|
||||
{label: __("Path"), slug: "path"},
|
||||
{label: __("Duration (ms)"), slug: "duration", sortable: true, number: true},
|
||||
{label: __("Time in Queries (ms)"), slug: "time_queries", sortable: true, number: true},
|
||||
{label: __("Queries"), slug: "queries", sortable: true, number: true},
|
||||
{label: __("Method"), slug: "method"},
|
||||
{label: __("Time"), slug: "time", sortable: true},
|
||||
];
|
||||
|
||||
const file_reader = new FileReader();
|
||||
file_reader.readAsText(request_file, 'UTF-8');
|
||||
file_reader.onload = ({target: {result}}) => {
|
||||
this.requests = JSON.parse(result);
|
||||
}
|
||||
}
|
||||
let query = ref({
|
||||
sort: "duration",
|
||||
order: "desc",
|
||||
filters: {},
|
||||
pagination: {
|
||||
limit: 20,
|
||||
page: 1,
|
||||
total: 0,
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.list-row .level-left {
|
||||
flex: 8;
|
||||
width: 100%;
|
||||
});
|
||||
|
||||
let status = ref({
|
||||
color: "grey",
|
||||
status: "Unknown",
|
||||
});
|
||||
|
||||
// Started
|
||||
frappe.recorder.router = router.value;
|
||||
let route = frappe.get_route();
|
||||
if (route[2]) {
|
||||
router.value.push({name: 'RequestDetail', params: {id: route[2]}});
|
||||
}
|
||||
|
||||
// Methods
|
||||
function filtered(reqs) {
|
||||
reqs = reqs.slice();
|
||||
const filters = Object.entries(query.value.filters);
|
||||
reqs = reqs.filter(
|
||||
(r) => filters.map((f) => (r[f[0]] || "").match(f[1])).every(Boolean)
|
||||
);
|
||||
query.value.pagination.total = Math.ceil(reqs.length / query.value.pagination.limit);
|
||||
return reqs;
|
||||
}
|
||||
function paginated(reqs) {
|
||||
reqs = reqs.slice();
|
||||
const begin = (query.value.pagination.page - 1) * (query.value.pagination.limit);
|
||||
const end = begin + query.value.pagination.limit;
|
||||
return reqs.slice(begin, end);
|
||||
}
|
||||
function sorted(reqs) {
|
||||
reqs = reqs.slice();
|
||||
const order = (query.value.order == "asc") ? 1 : -1;
|
||||
const sort = query.value.sort;
|
||||
return reqs.sort((a,b) => (a[sort] > b[sort]) ? order : -order);
|
||||
}
|
||||
function refresh() {
|
||||
frappe.call("frappe.recorder.get").then( r => requests.value = r.message);
|
||||
}
|
||||
function update(message) {
|
||||
requests.value.push(JSON.parse(message));
|
||||
}
|
||||
function clear() {
|
||||
frappe.call("frappe.recorder.delete").then(r => refresh());
|
||||
}
|
||||
function start() {
|
||||
frappe.call("frappe.recorder.start").then(r => fetch_status());
|
||||
}
|
||||
function stop() {
|
||||
frappe.call("frappe.recorder.stop").then(r => fetch_status());
|
||||
}
|
||||
function fetch_status() {
|
||||
frappe.call("frappe.recorder.status").then(r => update_status(r.message));
|
||||
}
|
||||
function update_status(result) {
|
||||
if(result) {
|
||||
status.value = {status: "Active", color: "green"}
|
||||
} else {
|
||||
status.value = {status: "Inactive", color: "red"}
|
||||
}
|
||||
page.set_indicator(status.value.status, status.value.color);
|
||||
if(status.value.status == "Active") {
|
||||
frappe.realtime.on("recorder-dump-event", update);
|
||||
} else {
|
||||
frappe.realtime.off("recorder-dump-event", update);
|
||||
}
|
||||
|
||||
update_buttons();
|
||||
}
|
||||
function update_buttons() {
|
||||
if(status.value.status == "Active") {
|
||||
page.set_primary_action(__("Stop"), () => {
|
||||
stop();
|
||||
});
|
||||
} else {
|
||||
page.set_primary_action(__("Start"), () => {
|
||||
start();
|
||||
});
|
||||
}
|
||||
}
|
||||
function route_to_request_detail(request) {
|
||||
router.value.beforeEach(async to => {
|
||||
if (to.meta.shouldFetch) {
|
||||
to.meta.request = await request
|
||||
}
|
||||
});
|
||||
router.value.push({name: 'RequestDetail', params: {id: request.uuid}});
|
||||
}
|
||||
function export_data() {
|
||||
if (!requests.value) {
|
||||
return;
|
||||
}
|
||||
frappe.call("frappe.recorder.export_data")
|
||||
.then((r) => {
|
||||
const data = r.message;
|
||||
const filename = `${data[0]['uuid']}..${data[data.length -1]['uuid']}.json`
|
||||
|
||||
const el = document.createElement('a');
|
||||
el.setAttribute('href', 'data:application/json,' + encodeURIComponent(JSON.stringify(data)));
|
||||
el.setAttribute('download', filename);
|
||||
el.click();
|
||||
});
|
||||
}
|
||||
function import_data(e) {
|
||||
if (requests.value.length > 0) {
|
||||
// don't replace existing capture
|
||||
return;
|
||||
}
|
||||
const request_file = e.dataTransfer.files[0];
|
||||
|
||||
const file_reader = new FileReader();
|
||||
file_reader.readAsText(request_file, 'UTF-8');
|
||||
file_reader.onload = ({target: {result}}) => {
|
||||
requests.value = JSON.parse(result);
|
||||
}
|
||||
}
|
||||
|
||||
// Mounted
|
||||
onMounted(() => {
|
||||
fetch_status();
|
||||
refresh();
|
||||
page.set_secondary_action(__("Clear"), () => {
|
||||
frappe.set_route("recorder");
|
||||
clear();
|
||||
});
|
||||
page.add_menu_item("Export data", () => export_data());
|
||||
});
|
||||
|
||||
// Computed
|
||||
let pages = computed(() => {
|
||||
const current_page = query.value.pagination.page;
|
||||
const total_pages = query.value.pagination.total;
|
||||
return [{
|
||||
label: __("First"),
|
||||
number: 1,
|
||||
status: (current_page == 1) ? "disabled" : "",
|
||||
},{
|
||||
label: __("Previous"),
|
||||
number: Math.max(current_page - 1, 1),
|
||||
status: (current_page == 1) ? "disabled" : "",
|
||||
}, {
|
||||
label: current_page,
|
||||
number: current_page,
|
||||
status: "btn-info",
|
||||
}, {
|
||||
label: __("Next"),
|
||||
number: Math.min(current_page + 1, total_pages),
|
||||
status: (current_page == total_pages) ? "disabled" : "",
|
||||
}, {
|
||||
label: __("Last"),
|
||||
number: total_pages,
|
||||
status: (current_page == total_pages) ? "disabled" : "",
|
||||
}];
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.list-row .level-left {
|
||||
flex: 8;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
<template>
|
||||
<keep-alive include="RecorderDetail">
|
||||
<router-view/>
|
||||
</keep-alive>
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive>
|
||||
<component :is="Component"></component>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "RecorderRoot",
|
||||
watch: {
|
||||
$route() {
|
||||
frappe.router.current_route = frappe.router.parse();
|
||||
frappe.breadcrumbs.update();
|
||||
}
|
||||
}
|
||||
};
|
||||
<script setup>
|
||||
import { watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
let route = useRoute();
|
||||
|
||||
watch(route, () => {
|
||||
frappe.router.current_route = frappe.router.parse();
|
||||
frappe.breadcrumbs.update();
|
||||
frappe.recorder.route = route;
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
</div>
|
||||
<div class="row form-section visible-section">
|
||||
<div class="col-sm-10">
|
||||
<h6 class="form-section-heading uppercase">{{ __("SQL Queries") }}</h6>
|
||||
<h6 class="form-section-heading uppercase">{{ translated_string("SQL Queries") }}</h6>
|
||||
</div>
|
||||
<div class="col-sm-2 filter-list">
|
||||
<div class="sort-selector">
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
<div class="checkbox">
|
||||
<label>
|
||||
<span class="input-area"><input type="checkbox" class="input-with-feedback bold" data-fieldtype="Check" v-model="group_duplicates"></span>
|
||||
<span class="label-area small">{{ __("Group Duplicate Queries") }}</span>
|
||||
<span class="label-area small">{{ translated_string("Group Duplicate Queries") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -48,15 +48,15 @@
|
|||
<div class="grid-row">
|
||||
<div class="data-row row">
|
||||
<div class="row-index col col-xs-1">
|
||||
<span>{{ __("Index") }}</span></div>
|
||||
<span>{{ translated_string("Index") }}</span></div>
|
||||
<div class="col grid-static-col col-xs-6">
|
||||
<div class="static-area ellipsis">{{ __("Query") }}</div>
|
||||
<div class="static-area ellipsis">{{ translated_string("Query") }}</div>
|
||||
</div>
|
||||
<div class="col grid-static-col col-xs-2">
|
||||
<div class="static-area ellipsis text-right">{{ __("Duration (ms)") }}</div>
|
||||
<div class="static-area ellipsis text-right">{{ translated_string("Duration (ms)") }}"</div>
|
||||
</div>
|
||||
<div class="col grid-static-col col-xs-2">
|
||||
<div class="static-area ellipsis text-right">{{ __("Exact Copies") }}</div>
|
||||
<div class="static-area ellipsis text-right">{{ translated_string("Exact Copies") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -82,7 +82,7 @@
|
|||
<div class="recorder-form-in-grid" v-if="showing == call.index">
|
||||
<div class="grid-form-heading" @click="showing = null">
|
||||
<div class="toolbar grid-header-toolbar">
|
||||
<span class="panel-title">{{ __("SQL Query") }} #<span class="grid-form-row-index">{{ call.index }}</span></span>
|
||||
<span class="panel-title">{{ translated_string("SQL Query") }} #<span class="grid-form-row-index">{{ call.index }}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-form-body">
|
||||
|
|
@ -95,25 +95,25 @@
|
|||
<form>
|
||||
<div class="frappe-control">
|
||||
<div class="form-group">
|
||||
<div class="clearfix"><label class="control-label">{{ __("Query") }}</label></div>
|
||||
<div class="clearfix"><label class="control-label">{{ translated_string("Query") }}</label></div>
|
||||
<div class="control-value like-disabled-input for-description"><pre>{{ call.query }}</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="frappe-control input-max-width">
|
||||
<div class="form-group">
|
||||
<div class="clearfix"><label class="control-label">{{ __("Duration (ms)") }}</label></div>
|
||||
<div class="clearfix"><label class="control-label">{{ translated_string("Duration (ms)") }}"</label></div>
|
||||
<div class="control-value like-disabled-input">{{ call.duration }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="frappe-control input-max-width">
|
||||
<div class="form-group">
|
||||
<div class="clearfix"><label class="control-label">{{ __("Exact Copies") }}</label></div>
|
||||
<div class="clearfix"><label class="control-label">{{ translated_string("Exact Copies") }}</label></div>
|
||||
<div class="control-value like-disabled-input">{{ call.exact_copies }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="frappe-control">
|
||||
<div class="form-group">
|
||||
<div class="clearfix"><label class="control-label">{{ __("Stack Trace") }}</label></div>
|
||||
<div class="clearfix"><label class="control-label">{{ translated_string("Stack Trace") }}</label></div>
|
||||
<div class="control-value like-disabled-input for-description" style="overflow:auto">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
|
|
@ -122,11 +122,9 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="(row, index) in call.stack">
|
||||
<tr :key="index">
|
||||
<td v-for="key in ['filename', 'lineno', 'function']" :key="key">{{ row[key] }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
<tr v-for="(row, index) in call.stack" :key="index">
|
||||
<td v-for="key in ['filename', 'lineno', 'function']" :key="key">{{ row[key] }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
@ -134,7 +132,7 @@
|
|||
</div>
|
||||
<div class="frappe-control" v-if="call.explain_result[0]">
|
||||
<div class="form-group">
|
||||
<div class="clearfix"><label class="control-label">{{ __("SQL Explain") }}</label></div>
|
||||
<div class="clearfix"><label class="control-label">{{ translated_string("SQL Explain") }}</label></div>
|
||||
<div class="control-value like-disabled-input for-description" style="overflow:auto">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
|
|
@ -162,7 +160,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="request.calls.length == 0" class="grid-empty text-center">{{ __("No Data") }}</div>
|
||||
<div v-if="request.calls.length == 0" class="grid-empty text-center">{{ translated_string("No Data") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -192,112 +190,114 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "RequestDetail",
|
||||
data() {
|
||||
return {
|
||||
columns: [
|
||||
{label: __("Path"), slug: "path", type: "Data", class: "col-sm-6"},
|
||||
{label: __("CMD"), slug: "cmd", type: "Data", class: "col-sm-6"},
|
||||
{label: __("Time"), slug: "time", type: "Time", class: "col-sm-6"},
|
||||
{label: __("Duration (ms)"), slug: "duration", type: "Float", class: "col-sm-6"},
|
||||
{label: __("Number of Queries"), slug: "queries", type: "Int", class: "col-sm-6"},
|
||||
{label: __("Time in Queries (ms)"), slug: "time_queries", type: "Float", class: "col-sm-6"},
|
||||
{label: __("Request Headers"), slug: "headers", type: "Small Text", formatter: value => `<pre class="for-description like-disabled-input">${JSON.stringify(value, null, 4)}</pre>`, class: "col-sm-12"},
|
||||
{label: __("Form Dict"), slug: "form_dict", type: "Small Text", formatter: value => `<pre class="for-description like-disabled-input">${JSON.stringify(value, null, 4)}</pre>`, class: "col-sm-12"},
|
||||
],
|
||||
table_columns: [
|
||||
{label: __("Execution Order"), slug: "index", sortable: true},
|
||||
{label: __("Duration (ms)"), slug: "duration", sortable: true},
|
||||
{label: __("Exact Copies"), slug: "exact_copies", sortable: true},
|
||||
],
|
||||
query: {
|
||||
sort: "duration",
|
||||
order: "desc",
|
||||
pagination: {
|
||||
limit: 20,
|
||||
page: 1,
|
||||
total: 0,
|
||||
}
|
||||
},
|
||||
group_duplicates: false,
|
||||
showing: null,
|
||||
request: {
|
||||
calls: [],
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
pages: function() {
|
||||
const current_page = this.query.pagination.page;
|
||||
const total_pages = this.query.pagination.total;
|
||||
return [{
|
||||
label: __("First"),
|
||||
number: 1,
|
||||
status: (current_page == 1) ? "disabled" : "",
|
||||
},{
|
||||
label: __("Previous"),
|
||||
number: Math.max(current_page - 1, 1),
|
||||
status: (current_page == 1) ? "disabled" : "",
|
||||
}, {
|
||||
label: current_page,
|
||||
number: current_page,
|
||||
status: "btn-info",
|
||||
}, {
|
||||
label: __("Next"),
|
||||
number: Math.min(current_page + 1, total_pages),
|
||||
status: (current_page == total_pages) ? "disabled" : "",
|
||||
}, {
|
||||
label: __("Last"),
|
||||
number: total_pages,
|
||||
status: (current_page == total_pages) ? "disabled" : "",
|
||||
}];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
paginated: function(calls) {
|
||||
calls = calls.slice();
|
||||
this.query.pagination.total = Math.ceil(calls.length / this.query.pagination.limit);
|
||||
const begin = (this.query.pagination.page - 1) * (this.query.pagination.limit);
|
||||
const end = begin + this.query.pagination.limit;
|
||||
return calls.slice(begin, end);
|
||||
},
|
||||
sorted: function(calls) {
|
||||
calls = calls.slice();
|
||||
const order = (this.query.order == "asc") ? 1 : -1;
|
||||
const sort = this.query.sort;
|
||||
return calls.sort((a,b) => (a[sort] > b[sort]) ? order : -order);
|
||||
},
|
||||
grouped: function(calls) {
|
||||
if(this.group_duplicates) {
|
||||
calls = calls.slice();
|
||||
return calls.uniqBy(call => call["query"]);
|
||||
}
|
||||
return calls
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
frappe.breadcrumbs.add({
|
||||
type: 'Custom',
|
||||
label: __('Recorder'),
|
||||
route: '/app/recorder'
|
||||
});
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { translated_string } from './utils'
|
||||
|
||||
// variables
|
||||
let route = ref(useRoute());
|
||||
let columns = [
|
||||
{label: __("Path"), slug: "path", type: "Data", class: "col-sm-6"},
|
||||
{label: __("CMD"), slug: "cmd", type: "Data", class: "col-sm-6"},
|
||||
{label: __("Time"), slug: "time", type: "Time", class: "col-sm-6"},
|
||||
{label: __("Duration (ms)"), slug: "duration", type: "Float", class: "col-sm-6"},
|
||||
{label: __("Number of Queries"), slug: "queries", type: "Int", class: "col-sm-6"},
|
||||
{label: __("Time in Queries (ms)"), slug: "time_queries", type: "Float", class: "col-sm-6"},
|
||||
{label: __("Request Headers"), slug: "headers", type: "Small Text", formatter: value => `<pre class="for-description like-disabled-input">${JSON.stringify(value, null, 4)}</pre>`, class: "col-sm-12"},
|
||||
{label: __("Form Dict"), slug: "form_dict", type: "Small Text", formatter: value => `<pre class="for-description like-disabled-input">${JSON.stringify(value, null, 4)}</pre>`, class: "col-sm-12"},
|
||||
];
|
||||
let table_columns = [
|
||||
{label: __("Execution Order"), slug: "index", sortable: true},
|
||||
{label: __("Duration (ms)"), slug: "duration", sortable: true},
|
||||
{label: __("Exact Copies"), slug: "exact_copies", sortable: true},
|
||||
];
|
||||
let query = ref({
|
||||
sort: "duration",
|
||||
order: "desc",
|
||||
pagination: {
|
||||
limit: 20,
|
||||
page: 1,
|
||||
total: 0,
|
||||
}
|
||||
});
|
||||
let group_duplicates = ref(false);
|
||||
let showing = ref(null);
|
||||
let request = ref({
|
||||
calls: [],
|
||||
});
|
||||
|
||||
// Methods
|
||||
function paginated(calls) {
|
||||
calls = calls.slice();
|
||||
query.value.pagination.total = Math.ceil(calls.length / query.value.pagination.limit);
|
||||
const begin = (query.value.pagination.page - 1) * (query.value.pagination.limit);
|
||||
const end = begin + query.value.pagination.limit;
|
||||
return calls.slice(begin, end);
|
||||
}
|
||||
function sorted(calls) {
|
||||
calls = calls.slice();
|
||||
const order = (query.value.order == "asc") ? 1 : -1;
|
||||
const sort = query.value.sort;
|
||||
return calls.sort((a,b) => (a[sort] > b[sort]) ? order : -order);
|
||||
}
|
||||
function grouped(calls) {
|
||||
if(group_duplicates.value) {
|
||||
calls = calls.slice();
|
||||
return calls.uniqBy(call => call["query"]);
|
||||
}
|
||||
return calls
|
||||
}
|
||||
|
||||
// Computed
|
||||
let pages = computed(() => {
|
||||
const current_page = query.value.pagination.page;
|
||||
const total_pages = query.value.pagination.total;
|
||||
return [{
|
||||
label: __("First"),
|
||||
number: 1,
|
||||
status: (current_page == 1) ? "disabled" : "",
|
||||
},{
|
||||
label: __("Previous"),
|
||||
number: Math.max(current_page - 1, 1),
|
||||
status: (current_page == 1) ? "disabled" : "",
|
||||
}, {
|
||||
label: current_page,
|
||||
number: current_page,
|
||||
status: "btn-info",
|
||||
}, {
|
||||
label: __("Next"),
|
||||
number: Math.min(current_page + 1, total_pages),
|
||||
status: (current_page == total_pages) ? "disabled" : "",
|
||||
}, {
|
||||
label: __("Last"),
|
||||
number: total_pages,
|
||||
status: (current_page == total_pages) ? "disabled" : "",
|
||||
}];
|
||||
});
|
||||
|
||||
// Mounted
|
||||
onMounted(async () => {
|
||||
frappe.breadcrumbs.add({
|
||||
type: 'Custom',
|
||||
label: __('Recorder'),
|
||||
route: '/app/recorder'
|
||||
});
|
||||
|
||||
const req = route.value.meta.request;
|
||||
const id = route.value.params.id;
|
||||
if (req && (req.headers || req.form_dict || req.calls)) {
|
||||
// complete request data passed as parameter.
|
||||
request.value = req;
|
||||
} else {
|
||||
let r = await frappe.call({
|
||||
method: "frappe.recorder.get",
|
||||
args: {
|
||||
uuid: req?.uuid || id,
|
||||
}
|
||||
});
|
||||
request.value = r.message;
|
||||
}
|
||||
});
|
||||
|
||||
const request = this.$route.params.request;
|
||||
if (request.headers || request.form_dict || request.calls) {
|
||||
// complete request data passed as parameter.
|
||||
this.request = request;
|
||||
} else {
|
||||
frappe.call({
|
||||
method: "frappe.recorder.get",
|
||||
args: {
|
||||
uuid: request.uuid
|
||||
}
|
||||
}).then( r => {
|
||||
this.request = r.message
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
5
frappe/public/js/frappe/recorder/recorder.bundle.js
Normal file
5
frappe/public/js/frappe/recorder/recorder.bundle.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { createApp } from "vue";
|
||||
import RecorderRoot from "./RecorderRoot.vue";
|
||||
import router from "./router.js";
|
||||
|
||||
frappe.recorder.view = createApp(RecorderRoot).use(router).mount(".recorder-container");
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
import Vue from "vue/dist/vue.js";
|
||||
import VueRouter from "vue-router/dist/vue-router.js";
|
||||
|
||||
import RecorderRoot from "./RecorderRoot.vue";
|
||||
|
||||
import RecorderDetail from "./RecorderDetail.vue";
|
||||
import RequestDetail from "./RequestDetail.vue";
|
||||
|
||||
Vue.prototype.__ = window.__;
|
||||
Vue.prototype.frappe = window.frappe;
|
||||
|
||||
Vue.use(VueRouter);
|
||||
const routes = [
|
||||
{
|
||||
name: "recorder-detail",
|
||||
path: "/detail",
|
||||
component: RecorderDetail,
|
||||
},
|
||||
{
|
||||
name: "request-detail",
|
||||
path: "/request/:id",
|
||||
component: RequestDetail,
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
redirect: {
|
||||
name: "recorder-detail",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: "history",
|
||||
base: "/app/recorder/",
|
||||
routes: routes,
|
||||
});
|
||||
|
||||
frappe.recorder.view = new Vue({
|
||||
el: ".recorder-container",
|
||||
router: router,
|
||||
data: {
|
||||
page: frappe.pages["recorder"].page,
|
||||
},
|
||||
template: "<recorder-root/>",
|
||||
components: {
|
||||
RecorderRoot,
|
||||
},
|
||||
});
|
||||
28
frappe/public/js/frappe/recorder/router.js
Normal file
28
frappe/public/js/frappe/recorder/router.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { createWebHistory, createRouter } from "vue-router";
|
||||
import RecorderDetail from "./RecorderDetail.vue";
|
||||
import RequestDetail from "./RequestDetail.vue";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/detail",
|
||||
name: "RecorderDetail",
|
||||
component: RecorderDetail,
|
||||
},
|
||||
{
|
||||
path: "/request/:id",
|
||||
name: "RequestDetail",
|
||||
component: RequestDetail,
|
||||
meta: { shouldFetch: true },
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
redirect: "/detail",
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory("/app/recorder/"),
|
||||
routes,
|
||||
});
|
||||
|
||||
export default router;
|
||||
3
frappe/public/js/frappe/recorder/utils.js
Normal file
3
frappe/public/js/frappe/recorder/utils.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export function translated_string(string) {
|
||||
return __(string);
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
import "./frappe/recorder/recorder";
|
||||
Loading…
Add table
Reference in a new issue