incremental work on NBUI

This commit is contained in:
Jonathan Shook 2020-09-23 13:43:55 -05:00
parent 50c1cd2a1d
commit 69dbd0a6ed
11 changed files with 194 additions and 338 deletions

View File

@ -5,11 +5,6 @@
<v-btn to="/ui/run/" title="Run a workload">Run</v-btn>
<v-btn to="/ui/watch/" title="Watch workload status">Watch</v-btn>
<v-btn to="/docs/" title="Documentation">Docs</v-btn>
<v-btn
title="Give us your feedback!"
href="https://github.com/nosqlbench/nosqlbench/wiki/Submitting-Feedback">
<v-icon>mdi-lightbulb-on-outline</v-icon>
</v-btn>
</div>
</template>

View File

@ -1,11 +1,24 @@
<template>
<v-app-bar app fluid>
<!-- <v-app-bar app dark fluid dense flat>-->
<v-toolbar-title><slot></slot></v-toolbar-title>
<v-toolbar-title>
<slot></slot>
</v-toolbar-title>
<v-spacer></v-spacer>
<v-toolbar-items>
<app-selector></app-selector>
<workspace-selector></workspace-selector>
<v-row>
<v-col>
<v-btn icon large
title="Give us your feedback!"
href="https://github.com/nosqlbench/nosqlbench/wiki/Submitting-Feedback">
<v-icon>mdi-lightbulb-on-outline</v-icon>
</v-btn>
</v-col>
</v-row>
</v-toolbar-items>
</v-app-bar>

View File

@ -2,6 +2,8 @@
<div class="markdown-body" v-html="rendered"></div>
</template>
<!--previously: https://github.com/ravenq/markdown-it-vue/blob/master/src/markdown-it-vue.vue-->
<script>
import "markdown-it"
import "markdown-it-imsize"

View File

@ -9,8 +9,8 @@
v-model="new_workspace"
ref="new_workspace_input"
hint="workspace name"
@blur="commitWorkspace(new_workspace)"
@keydown.enter="commitWorkspace(new_workspace)"
@blur="initializeWorkspace(new_workspace)"
@keydown.enter="initializeWorkspace(new_workspace)"
@keydown.esc="cancelWorkspace()"
></v-text-field>
<!-- label="workspace"-->
@ -77,7 +77,7 @@ export default {
this.$refs.new_workspace_input.focus();
});
},
commitWorkspace: function ({$store}) {
initializeWorkspace: function ({$store}) {
// console.log("commit:" + JSON.stringify(this.new_workspace));
this.$store.dispatch("workspaces/activateWorkspace", this.new_workspace);
this.new_workspace = "";

View File

@ -114,36 +114,27 @@ export default {
}
}
</script>
<style>
.container {
min-height: 60vh;
display: flex;
justify-content: flex-start;
align-items: flex-start;
text-align: start;
margin: 0 auto 0 15px;
}
<style scoped>
/*.title {*/
/* font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont,*/
/* 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;*/
/* display: block;*/
/* font-weight: 300;*/
/* font-size: 100px;*/
/* color: #35495e;*/
/* letter-spacing: 1px;*/
/*}*/
.title {
font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
display: block;
font-weight: 300;
font-size: 100px;
color: #35495e;
letter-spacing: 1px;
}
/*.subtitle {*/
/* font-weight: 300;*/
/* font-size: 42px;*/
/* color: #526488;*/
/* word-spacing: 5px;*/
/* padding-bottom: 15px;*/
/*}*/
.subtitle {
font-weight: 300;
font-size: 42px;
color: #526488;
word-spacing: 5px;
padding-bottom: 15px;
}
.links {
padding-top: 15px;
}
/*.links {*/
/* padding-top: 15px;*/
/*}*/
</style>

View File

@ -1,52 +1,44 @@
<template>
<v-app>
<main-app-bar>NoSQLBench - Workload Builder</main-app-bar>
<v-layout>
<main-app-bar>NoSQLBench - Workload Builder</main-app-bar>
<v-row>
<v-main>
<v-container fluid>
<v-layout row>
<v-flex>
<v-card>
<v-card-title>
Workload details
</v-card-title>
<v-col
cols="12"
sm="6"
md="10"
lg="10"
>
<v-text-field
outlined
label="Workload name"
v-model="workloadName"
></v-text-field>
<v-row>
<v-alert
v-if="!enabled"
>This component is not online. This is only a preview. To use this, you must be running a local instance of NoSQLBench in appserver mode.</v-alert>
</v-row>
<v-textarea
outlined
label="Create Table Statement"
v-model="createTableDef"
v-on:blur="parseStatement()"
></v-textarea>
<v-row class="d-flex justify-center">
<v-btn
:disabled="buildmode==='cql'"
title="Build CQL workload from schema"
@click="buildmode=(buildmode==='cql' ? null : 'cql')"
>CQL
</v-btn>
</v-col>
<v-btn
:disabled="buildmode==='openapi'"
title="Build OpenAPI workload from OpenAPI spec"
@click="buildmode='openapi'"
>OpenAPI
</v-btn>
</v-row>
<v-col cols="12">
<v-btn :title="save_title" v-if="parseSuccess" v-on:click="saveWorkloadToWorkspace()">{{ save_button }}</v-btn>
<v-btn :title="dl_title" v-if="parseSuccess" v-on:click="downloadWorkload()">{{ dl_button }}</v-btn>
</v-col>
</v-card>
</v-flex>
</v-layout>
<v-row row v-if="buildmode==='openapi'">
<OpenApiBuilder></OpenApiBuilder>
</v-row>
<v-row row v-if="buildmode==='cql'">
<CqlBuilder></CqlBuilder>
</v-row>
</v-container>
</v-main>
</v-layout>
</v-row>
<v-footer app>
<span>&copy; 2020</span>
@ -55,222 +47,35 @@
</v-app>
</template>
<script>
import antlr4 from "antlr4";
import {saveAs} from "file-saver";
import yamlDumper from "js-yaml";
import CQL3Parser from '@/antlr/CQL3Parser.js';
import CQL3Lexer from '@/antlr/CQL3Lexer.js';
import defaultYaml from 'assets/default.yaml';
import basictypes from 'assets/basictypes.yaml';
import WorkspaceSelector from "@/components/WorkspaceSelector";
import AppSelector from "@/components/AppSelector";
import MainAppBar from "@/components/MainAppBar";
import CqlBuilder from "~/components/builders/CqlBuilder";
import OpenApiBuilder from "~/components/builders/OpenApiBuilder";
export default {
components: {
MainAppBar,
AppSelector,
WorkspaceSelector
WorkspaceSelector,
CqlBuilder,
OpenApiBuilder
},
data(context) {
let data = {
enabled: false,
createTableDef: "",
workloadName: "",
parseSuccess: false,
blob: null,
buildmode: 'cql',
};
return data;
},
computed: {
save_button: function () {
return "Save to workspace '" + this.$store.getters["workspaces/getWorkspace"] + "'";
},
dl_button: function () {
return "Download as " + this.filename;
},
dl_title: function () {
return "Click to download the workload as '" + this.filename + "'";
},
filename: function () {
return this.workloadName + ".yaml";
},
save_title: function () {
return "Click to save this workload in the '" + this.workspace + "' workspace, or change the workspace in the app bar first.\n"
},
workspace: function () {
return this.$store.getters["workspaces/getWorkspace"]
},
enabled: function () {
return this.$store.getters["service_status/getEndpoints"]
async asyncData({store}) {
await store.dispatch("service_status/loadEndpoints")
return {
enabled: store.getters["service_status/getEnabled"]
}
},
methods: {
async parseStatement() {
console.log(this.$data.createTableDef);
const input = this.$data.createTableDef;
const chars = new antlr4.InputStream(input);
const lexer = new CQL3Lexer.CQL3Lexer(chars);
lexer.strictMode = false; // do not use js strictMode
const tokens = new antlr4.CommonTokenStream(lexer);
const parser = new CQL3Parser.CQL3Parser(tokens);
const context = parser.create_table_stmt();
try {
const keyspaceName = context.table_name().keyspace_name().getChild(0).getText()
const tableName = context.table_name().table_name_noks().getChild(0).getText()
const columnDefinitions = context.column_definitions().column_definition();
let columns = [];
let partitionKeys = [];
let clusteringKeys = [];
columnDefinitions.forEach(columnDef => {
if (columnDef.column_name() != null) {
columns.push({
"name": columnDef.column_name().getText(),
"type": columnDef.column_type().getText()
})
} else {
const primaryKeyContext = columnDef.primary_key()
if (primaryKeyContext.partition_key() != null) {
const partitionKeysContext = primaryKeyContext.partition_key().column_name();
partitionKeysContext.map((partitionKey, i) => {
const partitionKeyName = partitionKey.getText()
const col = {
"name": partitionKeyName,
"type": columns.filter(x => x.name == partitionKeyName)[0].type
}
partitionKeys.push(col)
})
}
if (primaryKeyContext.clustering_column().length != 0) {
const clusteringKeysContext = primaryKeyContext.clustering_column();
clusteringKeysContext.map((clusteringKey, i) => {
const clusteringKeyName = clusteringKey.getText()
const col = {
"name": clusteringKeyName,
"type": columns.filter(x => x.name == clusteringKeyName)[0].type
}
clusteringKeys.push(col)
})
}
}
})
columns = columns.filter(col => {
return partitionKeys.filter(pk => pk.name == col.name).length == 0 && clusteringKeys.filter(cc => cc.name == col.name).length == 0
})
const allColumns = [].concat(columns, partitionKeys, clusteringKeys)
this.$data.tableName = tableName;
this.$data.keyspaceName = keyspaceName;
this.$data.columns = columns;
this.$data.clusteringKeys = clusteringKeys;
this.$data.partitionKeys = partitionKeys;
this.$data.allColumns = allColumns;
console.log(this.$data)
console.log(defaultYaml)
// schema and bindings
let createTableStatement = "CREATE TABLE IF NOT EXISTS <<keyspace:" + keyspaceName + ">>." + tableName + " (\n";
console.log(basictypes)
defaultYaml.bindings = {}
allColumns.forEach(column => {
let recipe = basictypes.bindings[column.type + "val"];
if (recipe == undefined) {
const chars = new antlr4.InputStream(column.type);
const lexer = new CQL3Lexer.CQL3Lexer(chars);
lexer.strictMode = false; // do not use js strictMode
const tokens = new antlr4.CommonTokenStream(lexer);
const parser = new CQL3Parser.CQL3Parser(tokens);
const typeContext = parser.column_type();
const collectionTypeContext = typeContext.data_type().collection_type();
const collectionType = collectionTypeContext.children[0].getText();
if (collectionType.toLowerCase() == "set") {
const type = collectionTypeContext.children[2].getText();
recipe = "Set(HashRange(1,<<set-count-" + column.name + ":5>>)," + basictypes.bindings[type + "val"] + ") -> java.util.Set"
} else if (collectionType.toLowerCase() == "list") {
const type = collectionTypeContext.children[2].getText();
recipe = "List(HashRange(1,<<list-count-" + column.name + ":5>>)," + basictypes.bindings[type + "val"] + ") -> java.util.List"
} else if (collectionType.toLowerCase() == "map") {
const type1 = collectionTypeContext.children[2].getText();
const type2 = collectionTypeContext.children[4].getText();
recipe = "Map(HashRange(1,<<map-count-" + column.name + ":5>>)," + basictypes.bindings[type1 + "val"] + "," + basictypes.bindings[type2 + "val"] + ") -> java.util.Map"
} else {
alert("Could not generate recipe for type: " + column.type + " for column: " + column.name)
}
}
defaultYaml.bindings[column.name] = recipe
createTableStatement = createTableStatement + column.name + " " + column.type + ",\n";
})
let pk = "PRIMARY KEY (("
pk = pk + partitionKeys.map(x => x.name).reduce((x, acc) => acc = acc + "," + x)
pk = pk + ")"
if (clusteringKeys.length > 0) {
pk = pk + "," + clusteringKeys.map(x => x.name).reduce((x, acc) => acc = acc + "," + x)
}
pk = pk + ")"
createTableStatement = createTableStatement + pk + "\n);"
defaultYaml.blocks[0].statements[0] = {"create-table": createTableStatement}
//rampup
let insertStatement = "INSERT INTO <<keyspace:" + keyspaceName + ">>." + tableName + " (\n";
insertStatement = insertStatement + allColumns.map(x => x.name).reduce((x, acc) => acc = acc + ",\n" + x) + "\n) VALUES (\n";
insertStatement = insertStatement + allColumns.map(x => "{" + x.name + "}").reduce((x, acc) => acc = acc + ",\n" + x) + "\n);"
defaultYaml.blocks[1].statements[0] = {"insert-rampup": insertStatement}
//main-write
defaultYaml.blocks[2].statements[0] = {"insert-main": insertStatement}
//main-read-partition
let readPartitionStatement = "SELECT * from <<keyspace:" + keyspaceName + ">>." + tableName + " WHERE ";
readPartitionStatement = readPartitionStatement + partitionKeys.map(x => x.name + "={" + x.name + "}").reduce((x, acc) => acc = acc + " AND " + x);
let readRowStatement = readPartitionStatement + ";";
if (clusteringKeys.length > 0) {
readPartitionStatement = readPartitionStatement + " AND " + clusteringKeys.map(x => x.name + "={" + x.name + "}").reduce((x, acc) => acc = acc + " AND " + x);
}
readPartitionStatement = readPartitionStatement + ";";
defaultYaml.blocks[3].statements[0] = {"read-partition": readPartitionStatement}
//main-read-row
defaultYaml.blocks[4].statements[0] = {"read-row": readRowStatement}
defaultYaml.description = this.$data.workloadName
const yamlOutputText = yamlDumper.dump(defaultYaml)
this.blob = new Blob([yamlOutputText], {type: "text/plain;charset=utf-8"});
this.parseSuccess = true;
} catch (e) {
console.log("blur, invalid create table def")
console.log(e)
}
},
downloadWorkload() {
saveAs(this.blob, this.$data.filename);
},
saveWorkloadToWorkspace() {
this.$store.dispatch("workspaces/putFile",{
workspace: this.workspace,
filename: this.filename,
content: this.blob
})
computed: {
workspace: function () {
return this.$store.getters["workspaces/getWorkspace"]
}
},
created() {
@ -279,33 +84,4 @@ export default {
}
</script>
<style>
/*.container {*/
/* margin: 0 auto;*/
/* display: flex;*/
/* justify-content: center;*/
/* align-items: center;*/
/* text-align: center;*/
/*}*/
/*.title {*/
/* font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont,*/
/* 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;*/
/* display: block;*/
/* font-weight: 300;*/
/* font-size: 100px;*/
/* color: #35495e;*/
/* letter-spacing: 1px;*/
/*}*/
/*.subtitle {*/
/* font-weight: 300;*/
/* font-size: 42px;*/
/* color: #526488;*/
/* word-spacing: 5px;*/
/* padding-bottom: 15px;*/
/*}*/
/*.links {*/
/* padding-top: 15px;*/
/*}*/
</style>

View File

@ -28,12 +28,10 @@
<v-btn-toggle v-model="toggle_builtins" @change="validateAndSearch()">
<v-btn :disabled="this.toggle_workspaces===undefined">
<v-container fluid class="d-flex">
<v-icon title="include built-in workloads">mdi-folder-open</v-icon>
<div class="ma-2">bundled</div>
<v-icon v-if="this.toggle_builtins===0">mdi-check</v-icon>
</v-container>
</v-btn>
</v-btn-toggle>

View File

@ -3,31 +3,42 @@ import {mapGetters} from "vuex";
import endpoints from "@/js/endpoints";
export const state = () => ({
endpoints: {},
enabled: false
endpoints: null,
enabled: null
});
export const getters = {
getEndpoints: (state, getters) => {
return state.endpoints;
},
getEnabled: (state, getters) => {
return state.enabled;
}
}
export const mutations = {
setEndpoints(state, endpoints) {
state.endpoints = endpoints;
},
setEnabled(state, enabled) {
state.enabled = enabled;
}
}
export const actions = {
async loadEndpoints(context, reason) {
console.log("loading endpoint status because '" + reason + "'")
await this.$axios.get(endpoints.url(document, context, "/services/status"))
.then(res => {
context.commit('setEndpoints', res)
})
.catch((e) => {
console.error("axios/nuxt status async error:" + e);
})
let enabled = context.getters["getEnabled"]
if (enabled === null || enabled === undefined) {
console.log("loading endpoint status because '" + reason + "'")
await this.$axios.get(endpoints.url(document, context, "/services/status"))
.then(res => {
context.commit('setEndpoints', res.data.endpoints)
context.commit('setEnabled', res.data.enabled)
})
.catch((e) => {
console.error("axios/nuxt status async error:" + e);
})
}
// else use cache defined status
}
};

View File

@ -5,7 +5,8 @@ import endpoints from "@/js/endpoints";
export const state = () => ({
workspace: 'default',
workspaces: [],
fileview: []
all_ws_files: [],
matching_ws_files: []
});
export const getters = {
@ -15,8 +16,11 @@ export const getters = {
getWorkspaces: (state, getters) => {
return state.workspaces;
},
getFileview: (state, getters) => {
return state.fileview;
getAllFiles: (state, getters) => {
return state.all_ws_files;
},
getMatchingFiles: (state, getters) => {
return state.matching_ws_files;
}
// ...mapGetters(['workspace','workspaces'])
@ -29,21 +33,73 @@ export const mutations = {
setWorkspaces(state, workspaces) {
state.workspaces = workspaces;
},
setFileview(state, fileview) {
state.fileview = fileview;
setAllFiles(state, files) {
state.all_ws_files = files;
},
setMatchingFiles(state, files) {
state.matching_ws_files = files;
}
};
export const actions = {
async importUrlToWorkspace(context, params) {
console.log("importUrlToWorkspace(ctx," + JSON.stringify(params, null, 2));
let workspace = params.workspace;
let import_url = params.import_url;
let import_as = params.import_as;
if (!workspace || !import_url || !import_as) {
throw("Unable to save file to workspace without params workspace, import_url, import_as");
}
this.$axios.$get(import_url)
.then(res => {
console.log('save url data:' + JSON.stringify(res, null, 2))
return res
}).then(data => {
context.dispatch("putFile", {
workspace: workspace,
filename: import_as,
content: data
}).catch((e) => {
throw "error while saving data:" + e
})
})
.catch((e) => {
throw "axios/nuxt workspaces async error:" + e
})
},
async setWorkspace(context, val) {
// console.log("committing setWorkspace:" + JSON.stringify(val));
context.commit('setWorkspace', val);
await context.dispatch("listWorkspaceFiles", {wsname: val})
},
async setWorkspaces(context, val) {
// console.log("committing setWorkspaces:" + JSON.stringify(val));
context.commit('setWorkspaces', val);
},
async initWorkspaces(context, reason) {
async listWorkspaceFiles(context, params) {
let wsname = params.wsname;
let contains = params.contains;
console.log("list params:" + JSON.stringify(params, null, 2))
let query = "?ls";
if (contains) {
query = query + "&contains=" + contains
}
await this.$axios.$get(endpoints.url(document, context, "/services/workspaces/" + wsname) + query)
.then(res => {
console.log("ls ws:" + JSON.stringify(res, null, 2))
if (contains) {
context.commit("setContains", res["ls"]);
} else {
context.commit("setFileview", res["ls"])
}
})
.catch((e) => {
throw "axios/nuxt workspaces async error:" + e
})
},
async loadWorkspaces(context, reason) {
// console.log("initializing workspaces because '" + reason + "'")
this.$axios.$get(endpoints.url(document, context, "/services/workspaces/"))
.then(res => {
@ -52,7 +108,7 @@ export const actions = {
context.commit('setWorkspaces', res)
})
.catch((e) => {
console.error("axios/nuxt workspaces async error:", e);
throw "axios/nuxt workspaces async error:" + e
})
},
async putFile(context, params) {
@ -60,25 +116,27 @@ export const actions = {
let to_filename = params.filename;
let to_content = params.content;
if (!to_workspace || !to_filename || !to_content) {
console.log("params:" + JSON.stringify(params, null, 2))
throw("Unable to save file to workspace without params having workspace, filename, content");
}
const result = await this.$axios.$post(endpoints.url(document, context, "/services/workspaces/" + to_workspace + "/" + to_filename, to_content))
console.log("to_content:" + JSON.stringify(to_content, null, 2))
const result = await this.$axios.put(endpoints.url(document, context, "/services/workspaces/" + to_workspace + "/" + to_filename), to_content)
.then(res => {
console.log("axios/vuex workspace put:" + JSON.stringify(res));
return res;
})
.catch((e) => {
console.error("axios/vuex workspace put:", e)
throw "axios/vuex workspace put:" + e
});
},
async activateWorkspace(context, workspace) {
const fresh_workspace = await this.$axios.$get(endpoints.url(document, context, "/services/workspaces/" + workspace))
async initializeWorkspace(context, workspace) {
const fresh_workspace = await this.$axios.$get(endpoints.url(document, context, "/services/workspaces/" + workspace) + "?ls")
.then(res => {
// console.log("axios/vuex workspace async get:" + JSON.stringify(res))
return res;
})
.catch((e) => {
console.error("axios/nuxt getWorkspace async error:", e)
throw "axios/nuxt getWorkspace async error:" + e
})
await context.dispatch('initWorkspaces', "workspace '" + workspace + "' added");
// await dispatch.initWorkspaces({commit, state, dispatch}, "workspace '" + workspace + "' added")
@ -94,7 +152,7 @@ export const actions = {
return res;
})
.catch((e) => {
console.error("axios/nuxt purgeWorkspace error:", e)
throw "axios/nuxt purgeWorkspace error:" + e
})
const found = this.state.workspaces.workspaces.find(w => w.name === workspace);
if (!found) {

View File

@ -16,6 +16,7 @@ import javax.ws.rs.core.*;
import java.nio.ByteBuffer;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
@Path("/services/workspaces")
@Singleton
@ -79,7 +80,7 @@ public class WorkspacesEndpoint implements WebServiceObject {
return Response.ok().build();
}
@POST
@PUT
@Path("/{workspaceName}/{filepath:.+}")
@Consumes(MediaType.WILDCARD)
@Produces(MediaType.WILDCARD)
@ -103,17 +104,20 @@ public class WorkspacesEndpoint implements WebServiceObject {
@Produces(MediaType.APPLICATION_JSON)
public Response getWorkspaceInfo(
@PathParam("workspace") String workspace,
@QueryParam("ls") String ls
@QueryParam("ls") String ls,
@QueryParam("contains") String contains
) {
try {
if (ls!=null && !ls.toLowerCase().equals("false")) {
WorkSpace ws = getSvc().getWorkspace(workspace);
WorkSpace ws = getSvc().getWorkspace(workspace);
WorkspaceView wsview = ws.getWorkspaceView();
if (ls != null && !ls.toLowerCase().equals("false")) {
List<WorkspaceItemView> listing = ws.getWorkspaceListingView("");
return Response.ok(listing).build();
} else {
WorkspaceView workpaceView = getSvc().getWorkspaceView(workspace);
return Response.ok(workpaceView).build();
if (contains != null) {
listing = listing.stream().filter(i -> i.contains(contains)).collect(Collectors.toList());
}
wsview.setListing(listing);
}
return Response.ok(wsview).build();
} catch (Exception e) {
return Response.serverError().entity(e.getMessage()).build();
}
@ -128,7 +132,7 @@ public class WorkspacesEndpoint implements WebServiceObject {
@QueryParam("ls") String ls) {
try {
if (ls!=null && !ls.toLowerCase().equals("false")) {
if (ls != null && !ls.toLowerCase().equals("false")) {
WorkSpace ws = getSvc().getWorkspace(workspace);
List<WorkspaceItemView> listing = ws.getWorkspaceListingView(filename);
return Response.ok(listing).build();

View File

@ -11,6 +11,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
public class WorkspaceView {
@ -25,6 +26,9 @@ public class WorkspaceView {
private final Path workspaceRoot;
private Summary summary;
@JsonProperty("ls")
private List<WorkspaceItemView> listing = null;
public WorkspaceView(Path workspaceRoot) {
this.workspaceRoot = workspaceRoot;
}
@ -56,13 +60,17 @@ public class WorkspaceView {
return this.summary;
}
public void setListing(List<WorkspaceItemView> listing) {
this.listing = listing;
}
public final static class Summary extends SimpleFileVisitor<Path> {
private final Path root;
public long total_bytes = 0L;
public long total_files = 0L;
public long last_changed_epoch =Long.MIN_VALUE;
public long last_changed_epoch = Long.MIN_VALUE;
public String last_changed_filename = "";
public String getLast_changed_ago() {