view filters operational

This commit is contained in:
Martin McKeaveney 2020-08-24 11:46:28 +01:00
parent 1c4edea28f
commit 4d262b57ab
11 changed files with 91 additions and 87 deletions

View File

@ -127,19 +127,22 @@ export const getBackendUiStore = () => {
await store.actions.models.fetch() await store.actions.models.fetch()
}, },
save: async view => { save: async view => {
await api.post(`/api/views`, view) const response = await api.post(`/api/views`, view)
const viewMeta = await response.json()
store.update(state => { store.update(state => {
const viewModel = state.models.find( const viewModel = state.models.find(
model => model._id === view.modelId model => model._id === view.modelId
) )
// TODO: Cleaner?
if (!viewModel.views) viewModel.views = {}
if (view.originalName) delete viewModel.views[view.originalName] if (view.originalName) delete viewModel.views[view.originalName]
viewModel.views[view.name] = view viewModel.views[view.name] = viewMeta
state.models = state.models state.models = state.models
state.selectedView = view state.selectedView = {
name: view.name,
...viewMeta
}
return state return state
}) })
}, },

View File

@ -16,7 +16,7 @@
import EditRowPopover from "./popovers/EditRow.svelte" import EditRowPopover from "./popovers/EditRow.svelte"
import CalculationPopover from "./popovers/Calculate.svelte" import CalculationPopover from "./popovers/Calculate.svelte"
export let columns = [] export let schema = []
export let data = [] export let data = []
export let title export let title
@ -24,6 +24,7 @@
let currentPage = 0 let currentPage = 0
$: columns = schema ? Object.keys(schema) : []
$: paginatedData = $: paginatedData =
data && data.length data && data.length
? data.slice( ? data.slice(
@ -46,7 +47,7 @@
<thead> <thead>
<tr> <tr>
{#each columns as header} {#each columns as header}
<th>{header.name}</th> <th>{header}</th>
{/each} {/each}
</tr> </tr>
</thead> </thead>
@ -57,7 +58,7 @@
{#each paginatedData as row} {#each paginatedData as row}
<tr> <tr>
{#each columns as header} {#each columns as header}
<td>{getOr(row.default || '', header.key, row)}</td> <td>{getOr('', header, row)}</td>
{/each} {/each}
</tr> </tr>
{/each} {/each}

View File

@ -19,57 +19,28 @@
import GroupByPopover from "./popovers/GroupBy.svelte" import GroupByPopover from "./popovers/GroupBy.svelte"
import FilterPopover from "./popovers/Filter.svelte" import FilterPopover from "./popovers/Filter.svelte"
let COLUMNS = [
{
name: "group",
key: "key",
default: "All Records",
},
{
name: "sum",
key: "value.sum",
},
{
name: "min",
key: "value.min",
},
{
name: "max",
key: "value.max",
},
{
name: "sumsqr",
key: "value.sumsqr",
},
{
name: "count",
key: "value.count",
},
{
name: "avg",
key: "value.avg",
},
]
export let view = {} export let view = {}
let data = [] let data = []
$: ({ name, groupBy, filters } = view) $: ({ name, groupBy, filters, field } = view)
$: !name.startsWith("all_") && filters && fetchViewData(name, groupBy) $: !name.startsWith("all_") && filters && fetchViewData(name, field, groupBy)
async function fetchViewData(name, groupBy) { async function fetchViewData(name, field, groupBy) {
let QUERY_VIEW_URL = `/api/views/${name}?stats=true` const params = new URLSearchParams();
if (groupBy) {
QUERY_VIEW_URL += `&group=${groupBy}` if (field) params.set("stats", true);
} if (groupBy) params.set("group", groupBy);
let QUERY_VIEW_URL = `/api/views/${name}?${params}`
const response = await api.get(QUERY_VIEW_URL) const response = await api.get(QUERY_VIEW_URL)
data = await response.json() data = await response.json()
} }
</script> </script>
<Table title={decodeURI(view.name)} columns={COLUMNS} {data}> <Table title={decodeURI(view.name)} schema={view.schema} {data}>
<FilterPopover {view} /> <FilterPopover {view} />
<CalculationPopover {view} /> <CalculationPopover {view} />
<GroupByPopover {view} /> <GroupByPopover {view} />

View File

@ -15,7 +15,7 @@
{ {
name: "Statistics", name: "Statistics",
key: "stats", key: "stats",
}, }
] ]
export let view = {} export let view = {}
@ -50,12 +50,14 @@
<div class="input-group-row"> <div class="input-group-row">
<p>The</p> <p>The</p>
<Select secondary thin bind:value={view.calculation}> <Select secondary thin bind:value={view.calculation}>
<option value={null} />
{#each CALCULATIONS as calculation} {#each CALCULATIONS as calculation}
<option value={calculation.key}>{calculation.name}</option> <option value={calculation.key}>{calculation.name}</option>
{/each} {/each}
</Select> </Select>
<p>of</p> <p>of</p>
<Select secondary thin bind:value={view.field}> <Select secondary thin bind:value={view.field}>
<option value={null} />
{#each fields as field} {#each fields as field}
<option value={field}>{field}</option> <option value={field}>{field}</option>
{/each} {/each}

View File

@ -53,7 +53,7 @@
let anchor let anchor
let dropdown let dropdown
let filters = view.filters let filters = view.filters || []
$: viewModel = $backendUiStore.models.find( $: viewModel = $backendUiStore.models.find(
({ _id }) => _id === $backendUiStore.selectedView.modelId ({ _id }) => _id === $backendUiStore.selectedView.modelId

View File

@ -51,11 +51,6 @@
<h5>Create View</h5> <h5>Create View</h5>
<div class="input-group-column"> <div class="input-group-column">
<Input placeholder="View Name" thin bind:value={name} /> <Input placeholder="View Name" thin bind:value={name} />
<Select thin secondary bind:value={field}>
{#each fields as field}
<option value={field}>{field}</option>
{/each}
</Select>
</div> </div>
<div class="button-group"> <div class="button-group">
<Button secondary on:click={dropdown.hide}>Cancel</Button> <Button secondary on:click={dropdown.hide}>Cancel</Button>

View File

@ -87,12 +87,11 @@ exports.fetchView = async function(ctx) {
}) })
if (stats) { if (stats) {
for (let row of response.rows) { response.rows = response.rows.map(row => ({
row.value = { group: row.key,
...row.value, ...row.value,
avg: row.value.sum / row.value.count, avg: row.value.sum / row.value.count,
} }))
}
} else { } else {
response.rows = response.rows.map(row => row.doc) response.rows = response.rows.map(row => row.doc)
} }

View File

@ -1,5 +1,5 @@
const CouchDB = require("../../../db") const CouchDB = require("../../../db")
const statsViewTemplate = require("./viewBuilder") const viewTemplate = require("./viewBuilder")
const controller = { const controller = {
fetch: async ctx => { fetch: async ctx => {
@ -25,15 +25,15 @@ const controller = {
}, },
save: async ctx => { save: async ctx => {
const db = new CouchDB(ctx.user.instanceId) const db = new CouchDB(ctx.user.instanceId)
const { originalName, ...newView } = ctx.request.body const { originalName, ...viewToSave } = ctx.request.body
const designDoc = await db.get("_design/database") const designDoc = await db.get("_design/database")
const view = statsViewTemplate(newView) const view = viewTemplate(viewToSave)
designDoc.views = { designDoc.views = {
...designDoc.views, ...designDoc.views,
[newView.name]: view, [viewToSave.name]: view,
} }
// view has been renamed // view has been renamed
@ -45,10 +45,11 @@ const controller = {
// add views to model document // add views to model document
const model = await db.get(ctx.request.body.modelId) const model = await db.get(ctx.request.body.modelId)
model.views = { if (!model.views) model.views = {}
...(model.views ? model.views : {}), if (!view.meta.schema) {
[newView.name]: view.meta, view.meta.schema = model.schema
} }
model.views[viewToSave.name] = view.meta
if (originalName) { if (originalName) {
delete model.views[originalName] delete model.views[originalName]
@ -56,8 +57,8 @@ const controller = {
await db.put(model) await db.put(model)
ctx.body = view ctx.body = model.views[viewToSave.name]
ctx.message = `View ${newView.name} saved successfully.` ctx.message = `View ${viewToSave.name} saved successfully.`
}, },
destroy: async ctx => { destroy: async ctx => {
const db = new CouchDB(ctx.user.instanceId) const db = new CouchDB(ctx.user.instanceId)

View File

@ -5,7 +5,7 @@ describe("viewBuilder", () => {
describe("Filter", () => { describe("Filter", () => {
it("creates a view with multiple filters and conjunctions", () => { it("creates a view with multiple filters and conjunctions", () => {
expect(statsViewTemplate({ expect(statsViewTemplate({
"name": "yeety", "name": "Test View",
"modelId": "14f1c4e94d6a47b682ce89d35d4c78b0", "modelId": "14f1c4e94d6a47b682ce89d35d4c78b0",
"filters": [{ "filters": [{
"value": "Test", "value": "Test",
@ -22,7 +22,12 @@ describe("viewBuilder", () => {
}) })
describe("Calculate", () => { describe("Calculate", () => {
expect(statsViewTemplate({
"name": "Calculate View",
"field": "myField",
"modelId": "14f1c4e94d6a47b682ce89d35d4c78b0",
"filters": []
})).toMatchSnapshot()
}) })
describe("Group By", () => { describe("Group By", () => {

View File

@ -9,6 +9,32 @@ const TOKEN_MAP = {
OR: "||", OR: "||",
} }
const SCHEMA_MAP = {
stats: {
group: {
type: "string"
},
sum: {
type: "number"
},
min: {
type: "number"
},
max: {
type: "number"
},
count: {
type: "number"
},
sumsqr: {
type: "number"
},
avg: {
type: "number"
}
}
}
/** /**
* Iterates through the array of filters to create a JS * Iterates through the array of filters to create a JS
* expression that gets used in a CouchDB view. * expression that gets used in a CouchDB view.
@ -42,35 +68,36 @@ function parseEmitExpression(field, groupBy) {
return `emit(doc._id);` return `emit(doc._id);`
} }
function statsViewTemplate({ field, modelId, groupBy, filters = [] }) { function viewTemplate({
const filterExpression = parseFilterExpression(filters) field,
modelId,
groupBy,
filters = [],
calculation
}) {
const parsedFilters = parseFilterExpression(filters)
const filterExpression = parsedFilters ? `&& ${parsedFilters}` : ""
const emitExpression = parseEmitExpression(field, groupBy) const emitExpression = parseEmitExpression(field, groupBy)
const reduction = field ? { reduce: "_stats" } : {}
return { return {
meta: { meta: {
field, field,
modelId, modelId,
groupBy, groupBy,
filters, filters,
schema: { schema: SCHEMA_MAP[calculation],
sum: "number", calculation
min: "number",
max: "number",
count: "number",
sumsqr: "number",
avg: "number",
},
}, },
map: `function (doc) { map: `function (doc) {
if (doc.modelId === "${modelId}" ${ if (doc.modelId === "${modelId}" ${filterExpression}) {
filterExpression ? `&& ${filterExpression}` : ""
}) {
${emitExpression} ${emitExpression}
} }
}`, }`,
reduce: "_stats", ...reduction
} }
} }
module.exports = statsViewTemplate module.exports = viewTemplate

File diff suppressed because one or more lines are too long