Optimise condition evaluation performance and add support for conditionally setting text color

This commit is contained in:
Andrew Kingston 2024-06-27 14:23:05 +01:00
parent c9bcda0bd5
commit 99b522b32d
No known key found for this signature in database
8 changed files with 185 additions and 85 deletions

View File

@ -24,6 +24,16 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const flipDuration = 130 const flipDuration = 130
const conditionOptions = [
{
label: "Update background color",
value: "backgroundColor",
},
{
label: "Update text color",
value: "textColor",
},
]
let tempValue = [] let tempValue = []
let drawer let drawer
@ -57,6 +67,7 @@
const addCondition = () => { const addCondition = () => {
const condition = { const condition = {
id: generate(), id: generate(),
metadataKey: conditionOptions[0].value,
operator: Constants.OperatorOptions.Equals.value, operator: Constants.OperatorOptions.Equals.value,
valueType: FieldType.STRING, valueType: FieldType.STRING,
} }
@ -132,10 +143,15 @@
> >
<Icon name="DragHandle" size="XL" /> <Icon name="DragHandle" size="XL" />
</div> </div>
<span>Set background color to</span> <Select
placeholder={null}
options={conditionOptions}
bind:value={condition.metadataKey}
/>
<span>to</span>
<ColorPicker <ColorPicker
value={condition.color} value={condition.metadataValue}
on:change={e => (condition.color = e.detail)} on:change={e => (condition.metadataValue = e.detail)}
/> />
<span>if value</span> <span>if value</span>
<Select <Select
@ -197,7 +213,7 @@
} }
.condition { .condition {
display: grid; display: grid;
grid-template-columns: auto auto auto auto 1fr 1fr 1fr auto auto; grid-template-columns: auto 1fr auto auto auto 1fr 1fr 1fr auto auto;
align-items: center; align-items: center;
grid-column-gap: var(--spacing-l); grid-column-gap: var(--spacing-l);
} }

View File

@ -20,8 +20,11 @@
if (selectedUser) { if (selectedUser) {
style += `--user-color :${selectedUser.color};` style += `--user-color :${selectedUser.color};`
} }
if (metadata?.background) { if (metadata?.backgroundColor) {
style += `--cell-background: ${metadata.background};` style += `--cell-background: ${metadata.backgroundColor};`
}
if (metadata?.textColor) {
style += `--cell-font-color: ${metadata.textColor};`
} }
return style return style
} }
@ -76,7 +79,7 @@
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
color: var(--spectrum-global-color-gray-800); color: var(--cell-font-color);
font-size: var(--cell-font-size); font-size: var(--cell-font-size);
gap: var(--cell-spacing); gap: var(--cell-spacing);
background: var(--cell-background); background: var(--cell-background);

View File

@ -231,6 +231,7 @@
--cell-spacing: 4px; --cell-spacing: 4px;
--cell-border: 1px solid var(--spectrum-global-color-gray-200); --cell-border: 1px solid var(--spectrum-global-color-gray-200);
--cell-font-size: 14px; --cell-font-size: 14px;
--cell-font-color: var(--spectrum-global-color-gray-800);
flex: 1 1 auto; flex: 1 1 auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -166,7 +166,7 @@
/* Don't show borders between cells in the sticky column */ /* Don't show borders between cells in the sticky column */
.sticky-column :global(.cell:not(:last-child)) { .sticky-column :global(.cell:not(:last-child)) {
border-right: none; border-right-color: transparent;
} }
.header { .header {

View File

@ -0,0 +1,125 @@
import { writable, get } from "svelte/store"
import { derivedMemo, QueryUtils } from "../../../utils"
export const createStores = () => {
const conditionMetadata = writable({})
return {
conditionMetadata,
}
}
export const deriveStores = context => {
const { columns } = context
// Derive and memoize the conditions present in our columns so that we only
// recompute condition metdata when absolutely necessary
const conditions = derivedMemo(columns, $columns => {
let newConditions = []
for (let column of $columns) {
for (let condition of column.conditions || []) {
newConditions.push({
...condition,
column: column.name,
type: column.schema.type,
})
}
}
return newConditions
})
return {
conditions,
}
}
export const initialise = context => {
const { conditionMetadata, conditions, rows } = context
// Evaluates an array of conditions against a certain row and returns the
// resultant metadata
const evaluateConditions = (row, conditions) => {
let metadata = { version: row._rev }
for (let condition of conditions) {
try {
let {
column,
type,
referenceValue,
operator,
metadataKey,
metadataValue,
} = condition
let value = row[column]
// Coerce values into correct types for primitives
if (type === "number") {
referenceValue = parseFloat(referenceValue)
value = parseFloat(value)
} else if (type === "datetime") {
if (referenceValue) {
referenceValue = new Date(referenceValue).toISOString()
}
if (value) {
value = new Date(value).toISOString()
}
} else if (type === "boolean") {
referenceValue = `${referenceValue}`.toLowerCase() === "true"
value = `${value}`.toLowerCase() === "true"
}
// Build lucene compatible condition expression
const luceneFilter = {
operator,
type,
field: "value",
value: referenceValue,
}
const query = QueryUtils.buildQuery([luceneFilter])
const result = QueryUtils.runQuery([{ value }], query)
if (result.length > 0) {
if (!metadata[column]) {
metadata[column] = {}
}
metadata[column][metadataKey] = metadataValue
}
} catch {
// Swallow
}
}
return metadata
}
// Recompute all metadata if conditions change
conditions.subscribe($conditions => {
console.log("recomputing all conditions")
let metadata = {}
if ($conditions.length) {
for (let row of get(rows)) {
metadata[row._id] = evaluateConditions(row, $conditions)
}
}
conditionMetadata.set(metadata)
})
// Recompute specific rows when they change
rows.subscribe($rows => {
const $conditions = get(conditions)
if (!$conditions.length) {
return
}
const metadata = get(conditionMetadata)
let updates = {}
for (let row of $rows) {
if (!row._rev || metadata[row._id]?.version !== row._rev) {
console.log("recompute row", row._id)
updates[row._id] = evaluateConditions(row, $conditions)
}
}
if (Object.keys(updates).length) {
conditionMetadata.update(state => ({
...state,
...updates,
}))
}
})
}

View File

@ -20,6 +20,7 @@ import * as Table from "./datasources/table"
import * as ViewV2 from "./datasources/viewV2" import * as ViewV2 from "./datasources/viewV2"
import * as NonPlus from "./datasources/nonPlus" import * as NonPlus from "./datasources/nonPlus"
import * as Cache from "./cache" import * as Cache from "./cache"
import * as Conditions from "./conditions"
const DependencyOrderedStores = [ const DependencyOrderedStores = [
Sort, Sort,
@ -33,6 +34,7 @@ const DependencyOrderedStores = [
Scroll, Scroll,
Validation, Validation,
Rows, Rows,
Conditions,
UI, UI,
Resize, Resize,
Viewport, Viewport,

View File

@ -5,56 +5,6 @@ import { getCellID, parseCellID } from "../lib/utils"
import { tick } from "svelte" import { tick } from "svelte"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import { sleep } from "../../../utils/utils" import { sleep } from "../../../utils/utils"
import { QueryUtils } from "../../../utils"
const evaluateConditions = (row, column) => {
if (!column.conditions?.length) {
return
}
for (let condition of column.conditions) {
try {
const type = column.schema.type
let value = row[column.name]
let referenceValue = condition.referenceValue
// Coerce values into correct types for primitives
if (type === "number") {
referenceValue = parseFloat(referenceValue)
value = parseFloat(value)
} else if (type === "datetime") {
if (referenceValue) {
referenceValue = new Date(referenceValue).toISOString()
}
if (value) {
value = new Date(value).toISOString()
}
} else if (type === "boolean") {
referenceValue = `${referenceValue}`.toLowerCase() === "true"
value = `${value}`.toLowerCase() === "true"
}
// Build lucene compatible condition expression
const luceneFilter = {
operator: condition.operator,
type,
field: "value",
value: referenceValue,
}
const query = QueryUtils.buildQuery([luceneFilter])
const result = QueryUtils.runQuery([{ value }], query)
if (result.length > 0) {
if (!row.__metadata) {
row.__metadata = {}
}
row.__metadata[column.name] = {
background: condition.color,
}
}
} catch {
// Swallow
}
}
}
export const createStores = () => { export const createStores = () => {
const rows = writable([]) const rows = writable([])
@ -91,29 +41,15 @@ export const createStores = () => {
} }
export const deriveStores = context => { export const deriveStores = context => {
const { rows, columns, rowChangeCache } = context const { rows } = context
// Enrich rows with an index property and any pending changes // Enrich rows with an index property and any pending changes
const enrichedRows = derived( const enrichedRows = derived(rows, $rows => {
[rows, rowChangeCache, columns], return $rows.map((row, idx) => ({
([$rows, $rowChangeCache, $columns]) => { ...row,
if (!$rows?.length || !$columns?.length) { __idx: idx,
return [] }))
} })
console.log("ENRICH ROWS", $rows, $rowChangeCache, $columns)
return $rows.map((row, idx) => {
let enriched = {
...row,
...$rowChangeCache[row._id],
__idx: idx,
}
for (let column of $columns) {
evaluateConditions(enriched, column)
}
return enriched
})
}
)
// Generate a lookup map to quick find a row by ID // Generate a lookup map to quick find a row by ID
const rowLookupMap = derived(enrichedRows, $enrichedRows => { const rowLookupMap = derived(enrichedRows, $enrichedRows => {

View File

@ -10,6 +10,8 @@ export const deriveStores = context => {
scrollLeft, scrollLeft,
width, width,
height, height,
rowChangeCache,
conditionMetadata,
} = context } = context
// Derive visible rows // Derive visible rows
@ -30,12 +32,27 @@ export const deriveStores = context => {
0 0
) )
const renderedRows = derived( const renderedRows = derived(
[rows, scrolledRowCount, visualRowCapacity], [
([$rows, $scrolledRowCount, $visualRowCapacity]) => { rows,
return $rows.slice( scrolledRowCount,
$scrolledRowCount, visualRowCapacity,
$scrolledRowCount + $visualRowCapacity rowChangeCache,
) conditionMetadata,
],
([
$rows,
$scrolledRowCount,
$visualRowCapacity,
$rowChangeCache,
$conditionMetadata,
]) => {
return $rows
.slice($scrolledRowCount, $scrolledRowCount + $visualRowCapacity)
.map(row => ({
...row,
...$rowChangeCache[row._id],
__metadata: $conditionMetadata[row._id],
}))
}, },
[] []
) )