view filters operational
This commit is contained in:
parent
ab5ab97d5e
commit
40bf90c745
|
@ -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
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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", () => {
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue