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()
},
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 => {
const viewModel = state.models.find(
model => model._id === view.modelId
)
// TODO: Cleaner?
if (!viewModel.views) viewModel.views = {}
if (view.originalName) delete viewModel.views[view.originalName]
viewModel.views[view.name] = view
viewModel.views[view.name] = viewMeta
state.models = state.models
state.selectedView = view
state.selectedView = {
name: view.name,
...viewMeta
}
return state
})
},

View File

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

View File

@ -19,57 +19,28 @@
import GroupByPopover from "./popovers/GroupBy.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 = {}
let data = []
$: ({ name, groupBy, filters } = view)
$: !name.startsWith("all_") && filters && fetchViewData(name, groupBy)
$: ({ name, groupBy, filters, field } = view)
$: !name.startsWith("all_") && filters && fetchViewData(name, field, groupBy)
async function fetchViewData(name, groupBy) {
let QUERY_VIEW_URL = `/api/views/${name}?stats=true`
if (groupBy) {
QUERY_VIEW_URL += `&group=${groupBy}`
}
async function fetchViewData(name, field, groupBy) {
const params = new URLSearchParams();
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)
data = await response.json()
}
</script>
<Table title={decodeURI(view.name)} columns={COLUMNS} {data}>
<Table title={decodeURI(view.name)} schema={view.schema} {data}>
<FilterPopover {view} />
<CalculationPopover {view} />
<GroupByPopover {view} />

View File

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

View File

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

View File

@ -51,11 +51,6 @@
<h5>Create View</h5>
<div class="input-group-column">
<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 class="button-group">
<Button secondary on:click={dropdown.hide}>Cancel</Button>

View File

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

View File

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

View File

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

View File

@ -9,6 +9,32 @@ const TOKEN_MAP = {
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
* expression that gets used in a CouchDB view.
@ -42,35 +68,36 @@ function parseEmitExpression(field, groupBy) {
return `emit(doc._id);`
}
function statsViewTemplate({ field, modelId, groupBy, filters = [] }) {
const filterExpression = parseFilterExpression(filters)
function viewTemplate({
field,
modelId,
groupBy,
filters = [],
calculation
}) {
const parsedFilters = parseFilterExpression(filters)
const filterExpression = parsedFilters ? `&& ${parsedFilters}` : ""
const emitExpression = parseEmitExpression(field, groupBy)
const reduction = field ? { reduce: "_stats" } : {}
return {
meta: {
field,
modelId,
groupBy,
filters,
schema: {
sum: "number",
min: "number",
max: "number",
count: "number",
sumsqr: "number",
avg: "number",
},
schema: SCHEMA_MAP[calculation],
calculation
},
map: `function (doc) {
if (doc.modelId === "${modelId}" ${
filterExpression ? `&& ${filterExpression}` : ""
}) {
if (doc.modelId === "${modelId}" ${filterExpression}) {
${emitExpression}
}
}`,
reduce: "_stats",
...reduction
}
}
module.exports = statsViewTemplate
module.exports = viewTemplate

File diff suppressed because one or more lines are too long