Skip to content

Commit

Permalink
Merge pull request #2899 from evidence-dev/release-2024-12-12
Browse files Browse the repository at this point in the history
Release release-2024-12-12
  • Loading branch information
zachstence authored Dec 12, 2024
2 parents db28a0f + 9fed0b5 commit e57b3b3
Show file tree
Hide file tree
Showing 11 changed files with 264 additions and 13 deletions.
6 changes: 6 additions & 0 deletions .changeset/odd-fans-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@evidence-dev/evidence': patch
'@evidence-dev/sdk': patch
---

fix environment variable display in settings page
5 changes: 5 additions & 0 deletions .changeset/warm-jars-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@evidence-dev/tailwind': patch
---

Dont merge color palettes/scales
1 change: 1 addition & 0 deletions packages/lib/sdk/src/plugins/datasources/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './loadSources.js';
export * from './loadSourcePlugins.js';
export * from './writeSourceConfig.js';
export * from './cli/edit/Options.js';
export { getDatasourceConfigAsEnvironmentVariables } from './loadSourceConfig.js';
25 changes: 25 additions & 0 deletions packages/lib/sdk/src/plugins/datasources/loadSourceConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,28 @@ export const loadSourceConfig = async (sourceDir) => {
options
};
};

/**
* @param {import('./schemas/datasource.schema.js').DatasourceSpec} datasource
*/
export const getDatasourceConfigAsEnvironmentVariables = (datasource) => {
/** @type {Record<string,string>} */
const environmentVariables = {};
/**
* @param {any} obj
* @param {string} [currentKey]
*/
const generateNestedEnvVars = (obj, currentKey = '') => {
for (const [key, value] of Object.entries(obj)) {
const newKey = currentKey ? `${currentKey}__${key}` : key;
if (typeof value === 'object') {
generateNestedEnvVars(value, newKey);
} else {
environmentVariables[`EVIDENCE_SOURCE__${datasource.name}__${newKey}`] = value.toString();
}
}
};
generateNestedEnvVars(datasource.options);

return environmentVariables;
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import defaultsDeep from 'lodash/defaultsDeep.js';
import merge from 'lodash/merge.js';
import mergeWith from 'lodash/mergeWith.js';
import { defaultThemesConfigFile } from './defaultThemesConfigFile.js';
import { computeShades } from './computeShades.js';

Expand All @@ -8,8 +8,14 @@ import { computeShades } from './computeShades.js';
* @returns {import('../../schemas/types.js').ThemesConfig}
*/
export const applyThemeDefaults = (configFile) => {
const withDefaults = defaultsDeep({}, configFile, defaultThemesConfigFile);
/** @satisfies {typeof defaultThemesConfigFile} */
const withDefaults = mergeWith({}, defaultThemesConfigFile, configFile, (_, configValue) => {
// Don't merge arrays - prevents merging users defined color palette with our defaults
if (Array.isArray(configValue) && configValue.length) {
return configValue;
}
});
const computedColors = computeShades(withDefaults.theme.colors);
merge(withDefaults.theme.colors, computedColors);
return withDefaults;
return /** @type {import('../../schemas/types.js').ThemesConfig} */ (withDefaults);
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// @ts-check

import { describe, it, expect } from 'vitest';
import chroma from 'chroma-js';

Expand Down Expand Up @@ -61,8 +63,8 @@ describe('applyThemeDefaults', () => {
'should have contrast >= 4.5 between $requiredColor and $computedColor in $mode mode',
({ requiredColor, computedColor, mode }) => {
const actual = applyThemeDefaults(input);
const requiredColorValue = actual.theme.colors[requiredColor][mode];
const computedColorValue = actual.theme.colors[computedColor][mode];
const requiredColorValue = actual.theme.colors[requiredColor]?.[mode];
const computedColorValue = actual.theme.colors[computedColor]?.[mode];
expect(
chroma.contrast(requiredColorValue, computedColorValue),
`Expected contrast between ${requiredColor} (${requiredColorValue}) and ${computedColor} (${computedColorValue}) to be greater than 4.5 in ${mode} mode`
Expand All @@ -81,8 +83,8 @@ describe('applyThemeDefaults', () => {
'should have contrast >= 4.5 between $bgColor and $fgColor in $mode mode',
({ bgColor, fgColor, mode }) => {
const actual = applyThemeDefaults(input);
const bgColorValue = actual.theme.colors[bgColor][mode];
const fgColorValue = actual.theme.colors[fgColor][mode];
const bgColorValue = actual.theme.colors[bgColor]?.[mode];
const fgColorValue = actual.theme.colors[fgColor]?.[mode];
expect(
chroma.contrast(bgColorValue, fgColorValue),
`Expected contrast between ${bgColor} (${bgColorValue}) and ${fgColor} (${fgColorValue}) to be greater than 7 in ${mode} mode`
Expand All @@ -107,4 +109,40 @@ describe('applyThemeDefaults', () => {
expect(actual.theme.colors.myCustomColor?.light).toBe('#abcdef');
expect(actual.theme.colors.myCustomColor?.dark).toBe('#fedcba');
});

it('should not merge default color palette into user configured default color palette', () => {
/** @satisfies {import('../../schemas/types.js').ThemesConfigFile} */
const config = {
theme: {
colorPalettes: {
default: {
light: ['colorPalettes_default_light_1', 'colorPalettes_default_light_2'],
dark: ['colorPalettes_default_dark_1', 'colorPalettes_default_dark_2']
}
}
}
};

const withDefaults = applyThemeDefaults(config);

expect(withDefaults.theme.colorPalettes.default).toEqual(config.theme.colorPalettes.default);
});

it('should not merge default color scale into user configured default color scale', () => {
/** @satisfies {import('../../schemas/types.js').ThemesConfigFile} */
const config = {
theme: {
colorScales: {
default: {
light: ['colorScales_default_light_1', 'colorScales_default_light_2'],
dark: ['colorScales_default_dark_1', 'colorScales_default_dark_2']
}
}
}
};

const withDefaults = applyThemeDefaults(config);

expect(withDefaults.theme.colorScales.default).toEqual(config.theme.colorScales.default);
});
});
161 changes: 161 additions & 0 deletions sites/docs/pages/components/custom-components/component-queries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Component Queries

Component queries allow you to run SQL queries in your component code.

Component queries transform how we build data visualizations. Instead of passing data down through props from parent pages, components become self-contained units that can request exactly what they need. This independence makes components more reusable and easier to maintain, as all the logic for both fetching and displaying data lives in one place.

## Static Queries

Static queries are "static" because the SQL string they run cannot change throughout the component's lifecycle. They are defined once when your component is created, and are executed when `QueryLoad` is mounted.

Here's how to create a component that fetches and displays information about tables in your database:

```html title="components/TableList.svelte"
&lt;script>
import { buildQuery } from '@evidence-dev/component-utilities/buildQuery';
import { QueryLoad } from '@evidence-dev/core-components';

const query = buildQuery(
'SELECT * FROM information_schema.tables',
);
</script>

<QueryLoad data={query} let:loaded={tables}>
<svelte:fragment slot="skeleton" />

<ul>
{#each tables as table}
<li>{table.table_name}</li>
{/each}
</ul>
</QueryLoad>
```

### The Query Loader

The `QueryLoad` component manages the entire lifecycle of your query execution. It handles:
- Executing your query against DuckDB
- Managing loading states
- Handling any errors that occur
- Delivering results to your component

```html
<QueryLoad data={query} let:loaded={tableData}>
<svelte:fragment slot="skeleton" />
<!-- Your component content here -->
</QueryLoad>
```

The `let:loaded` directive creates a new variable containing your query results. Using a descriptive name (like `tableData` or `salesMetrics`) makes your code more maintainable than the generic `loaded`.

## Dynamic Queries

For queries that change based on user input or component state, you need dynamic queries. This allows you to create interactive components.

Here's an example that lets users control how many rows to display:

```html title="components/DynamicTableList.svelte"
&lt;script>
import { QueryLoad } from '@evidence-dev/core-components';
import { getQueryFunction } from '@evidence-dev/component-utilities/buildQuery';
import { Query } from '@evidence-dev/sdk/usql';

// This will hold our current query result
let query;

// Create a reactive query function
const queryFunction = Query.createReactive({
execFn: getQueryFunction(),
callback: v => query = v
});

// These values will control our query
let limit = 10;
let schemaName = 'public';

// This reactive statement runs whenever limit or schemaName change
$: queryFunction(`
SELECT *
FROM information_schema.tables
WHERE table_schema = '${schemaName}'
LIMIT ${limit}
`);
</script>

<div>
<label>
Rows to show:
<input type="number" bind:value={limit} min={0} />
</label>
<label>
Schema:
<input type="text" bind:value={schemaName} />
</label>
</div>

<QueryLoad data={query} let:loaded={tables}>
<svelte:fragment slot="skeleton" />
<ul>
{#each tables as table}
<li>{table.table_name}</li>
{/each}
</ul>
</QueryLoad>
```

Let's understand how this works:

### Query State Management

1. First, we create a variable to hold our query result:
```javascript
let query;
```
This will be updated every time our query executes with new parameters.

2. Next, we create a reactive query function:
```javascript
const queryFunction = Query.createReactive({
execFn: getQueryFunction(),
callback: v => query = v
});
```
This sets up an environment that can execute queries and update our component's state.

3. Finally, we use Svelte's reactive declarations to run our query:
```javascript
$: queryFunction(`SELECT * FROM ... LIMIT ${limit}`);
```
The `$:` syntax tells Svelte to re-run this statement whenever `limit` changes, creating a connection between your component's state and your query.

## Error Handling

When working with queries, things can sometimes go wrong. Maybe a query is malformed, or perhaps it's trying to access a table that doesn't exist. The `QueryLoad` component helps you handle these situations gracefully through its error slot:

```html
<QueryLoad data={query} let:loaded={tables}>
<svelte:fragment slot="skeleton" />

<svelte:fragment slot="error" let:error>
<div class="text-red-600">
<h3 class="font-bold">Unable to load data</h3>
<p>{error.message}</p>
<p class="text-sm mt-2">
Please check your query and try again.
</p>
</div>
</svelte:fragment>

<ul>
{#each tables as table}
<li>{table.table_name}</li>
{/each}
</ul>
</QueryLoad>
```

When a query fails, Evidence:
1. Captures the error information
2. Prevents the main content from rendering
3. Makes the error details available through `let:error`
4. Displays your error handling content
4 changes: 4 additions & 0 deletions sites/docs/pages/components/custom-components/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,7 @@ import formatTitle from '@evidence-dev/component-utilities/formatTitle';
```
- `column`: name of column to be formatted
- `columnFormat`: a format object for column being formatted (can be obtained from the `getColumnSummary` function)
## Adding Queries
Custom components can execute queries rather than requiring data to be passed to them. See [component queries](./component-queries) for details
2 changes: 1 addition & 1 deletion sites/docs/pages/core-concepts/themes/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Evidence supports three appearance modes: light, dark, and system. By default, n

When the appearance is `system`, the user's preferred appearance from their operating system is used.

The default appearance configuration is listed below, as well as in the [Evidence Template](https://github.com/evidence-dev/templates/blob/main/evidence.config.yaml).
The default appearance configuration is listed below, as well as in the [Evidence Template](https://github.com/evidence-dev/template/blob/main/evidence.config.yaml).

<div id="default-appearance-configuration" class="block relative -top-16 invisible"></div>
<Details title='Default appearance configuration'>
Expand Down
13 changes: 10 additions & 3 deletions sites/example-project/src/pages/settings/+page.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
loadSourcePlugins,
DatasourceSpecFileSchema,
Options,
writeSourceConfig
writeSourceConfig,
getDatasourceConfigAsEnvironmentVariables
} from '@evidence-dev/sdk/plugins';

export const load = async () => {
Expand All @@ -15,12 +16,18 @@ export const load = async () => {
const datasources = await loadSourcePlugins();

const plugins = Object.entries(datasources.bySource).reduce((acc, [name, v]) => {
acc[name] = { package: { package: v[0] }, options: v[1].options };
acc[name] = {
package: { package: v[0] },
options: v[1].options
};
return acc;
}, {});

return {
sources,
sources: sources.map((source) => ({
...source,
environmentVariables: getDatasourceConfigAsEnvironmentVariables(source)
})),
plugins
};
}
Expand Down
2 changes: 0 additions & 2 deletions sites/example-project/src/pages/settings/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
let { settings, customFormattingSettings, sources, plugins } = data;
$: ({ settings, customFormattingSettings, sources, plugins } = data);
console.log({ settings, sources, plugins });
import { dev } from '$app/environment';
import {
Expand Down

0 comments on commit e57b3b3

Please sign in to comment.