Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions runtime/feature_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ var defaultFeatureFlags = map[string]string{
"custom_charts": "false",
// Controls visibility of the personal canvas feature (per-user owner-only canvases stored as virtual files)
"personal_canvases": "false",
// Controls visibility of the SQL Console
"sql_console": "false",
}

// ResolveFeatureFlags resolves feature flags for the given instance and the provided user attributes.
Expand Down
4 changes: 4 additions & 0 deletions runtime/feature_flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func Test_ResolveFeatureFlags(t *testing.T) {
"cloudEditing": false,
"customCharts": false,
"personalCanvases": false,
"sqlConsole": false,
"disablePersistentDashboardState": false,
},
},
Expand Down Expand Up @@ -74,6 +75,7 @@ func Test_ResolveFeatureFlags(t *testing.T) {
"cloudEditing": false,
"customCharts": false,
"personalCanvases": false,
"sqlConsole": false,
"disablePersistentDashboardState": false,
},
},
Expand Down Expand Up @@ -102,6 +104,7 @@ func Test_ResolveFeatureFlags(t *testing.T) {
"cloudEditing": false,
"customCharts": false,
"personalCanvases": false,
"sqlConsole": false,
"disablePersistentDashboardState": false,
},
},
Expand Down Expand Up @@ -130,6 +133,7 @@ func Test_ResolveFeatureFlags(t *testing.T) {
"cloudEditing": false,
"customCharts": false,
"personalCanvases": false,
"sqlConsole": false,
"disablePersistentDashboardState": false,
},
},
Expand Down
12 changes: 6 additions & 6 deletions web-admin/src/features/projects/ProjectTabs.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
export let pathname: string;
export let branchPrefix: string = "";

const { chat, reports, alerts } = featureFlags;
const { chat, reports, alerts, sqlConsole } = featureFlags;

$: tabs = [
{
Expand All @@ -33,11 +33,6 @@
label: m.nav_tab_dashboards(),
hasPermission: true,
},
{
route: `/${organization}/${project}${branchPrefix}/-/query`,
label: m.nav_tab_query(),
hasPermission: false,
},
{
route: `/${organization}/${project}${branchPrefix}/-/reports`,
label: m.nav_tab_reports(),
Expand All @@ -53,6 +48,11 @@
label: m.nav_tab_status(),
hasPermission: projectPermissions.manageProject,
},
{
route: `/${organization}/${project}${branchPrefix}/-/query`,
label: m.nav_tab_query(),
hasPermission: $sqlConsole && projectPermissions.manageProject,
},
{
route: `/${organization}/${project}${branchPrefix}/-/settings`,
label: m.nav_tab_settings(),
Expand Down
66 changes: 66 additions & 0 deletions web-admin/src/routes/[organization]/[project]/-/query/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<script lang="ts">
import { page } from "$app/state";
import { extractBranchFromPath } from "@rilldata/web-admin/features/branches/branch-utils";
import ErrorPage from "@rilldata/web-common/components/ErrorPage.svelte";
import Spinner from "@rilldata/web-common/features/entity-management/Spinner.svelte";
import { EntityStatus } from "@rilldata/web-common/features/entity-management/types";
import QueryWorkspace from "@rilldata/web-common/features/query/QueryWorkspace.svelte";
import { createRuntimeServiceGetInstance } from "@rilldata/web-common/runtime-client";
import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2";

const runtimeClient = useRuntimeClient();
const instanceQuery = createRuntimeServiceGetInstance(runtimeClient, {
sensitive: true,
});

let organization = $derived(page.params.organization);
let project = $derived(page.params.project);
let activeBranch = $derived(extractBranchFromPath(page.url.pathname));
let cacheKey = $derived(
`${organization}/${project}${activeBranch ? `@${activeBranch}` : ""}`,
);
let {
data: instanceData,
error: instanceError,
isLoading,
} = $derived($instanceQuery);
let olapConnector = $derived(instanceData?.instance?.olapConnector ?? "");
let sqlConsoleEnabled = $derived(
instanceData?.instance?.featureFlags?.sqlConsole ?? false,
);
</script>

<svelte:head>
<title>SQL Console · {project} · Rill</title>
</svelte:head>

<div class="size-full min-h-0 overflow-hidden">
{#if isLoading}
<div
class="flex size-full items-center justify-center gap-x-2 text-sm text-fg-secondary"
>
<Spinner size="20px" status={EntityStatus.Running} />
Loading SQL Console
</div>
{:else if instanceError}
<ErrorPage
statusCode={500}
header="Unable to load SQL Console"
body={instanceError.message}
/>
{:else if !sqlConsoleEnabled}
<ErrorPage
statusCode={404}
header="Page not found"
body="SQL Console is not enabled for this project."
/>
{:else if olapConnector}
<QueryWorkspace {cacheKey} connector={olapConnector} />
{:else}
<div
class="flex size-full items-center justify-center text-sm text-fg-secondary"
>
No project OLAP connector is available.
</div>
{/if}
</div>
12 changes: 12 additions & 0 deletions web-admin/src/routes/[organization]/[project]/-/query/+page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { redirect } from "@sveltejs/kit";
import type { PageLoad } from "./$types";

export const load: PageLoad = async ({
parent,
params: { organization, project },
}) => {
const { projectPermissions } = await parent();
if (!projectPermissions?.manageProject) {
throw redirect(307, `/${organization}/${project}`);
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,24 @@

export let store: ConnectorExplorerStore;
export let olapOnly: boolean = false;
/** When set, only display the connector with this name. */
export let onlyConnector: string = "";
/** Auto-expand this connector when the list first renders */
export let defaultExpanded: string = "";

const client = useRuntimeClient();

$: connectors = getAnalyzedConnectors(client, olapOnly);
$: ({ data, error, isLoading } = $connectors);
$: displayedConnectors = onlyConnector
? data?.connectors?.filter((connector) => connector.name === onlyConnector)
: data?.connectors;

// When defaultExpanded is set, pre-seed the store so only that connector
// starts expanded and others start collapsed.
let hasAutoExpanded = false;
$: if (defaultExpanded && data?.connectors && !hasAutoExpanded) {
for (const c of data.connectors) {
$: if (defaultExpanded && displayedConnectors && !hasAutoExpanded) {
for (const c of displayedConnectors) {
if (!c.name) continue;
// Pre-seed each connector before ConnectorEntry renders.
// This prevents getDefaultState from expanding all connectors.
Expand All @@ -44,19 +49,19 @@
<span class="message">
{error.message}
</span>
{:else if data?.connectors}
{#if data.connectors.length === 0}
{:else if displayedConnectors}
{#if displayedConnectors.length === 0}
<span class="message"> No data found. Add data to get started! </span>
{:else}
<ol transition:slide={{ duration }}>
{#each data.connectors as connector (connector.name)}
{#each displayedConnectors as connector (connector.name)}
<ConnectorEntry {connector} {store} />
{/each}
</ol>
{/if}
{:else if isLoading}
<div class="flex flex-col gap-y-1.5 w-full px-2 py-2">
{#each [0.6, 0.75, 0.5] as width}
{#each [0.6, 0.75, 0.5] as width (width)}
<div
class="h-5 bg-gray-200 animate-pulse rounded"
style:width="{width * 100}%"
Expand Down
1 change: 1 addition & 0 deletions web-common/src/features/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class FeatureFlags {
customCharts = new FeatureFlag("user", false);
disablePersistentDashboardState = new FeatureFlag("user", false);
personalCanvases = new FeatureFlag("user", false);
sqlConsole = new FeatureFlag("user", false);

private flagsUnsub?: () => void;

Expand Down
143 changes: 143 additions & 0 deletions web-common/src/features/query/QueryEditor.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<script lang="ts">
import { autocompletion } from "@codemirror/autocomplete";
import { keywordCompletionSource, sql } from "@codemirror/lang-sql";
import {
EditorState,
Prec,
StateEffect,
StateField,
} from "@codemirror/state";
import {
Decoration,
EditorView,
keymap,
type DecorationSet,
} from "@codemirror/view";
import { onMount } from "svelte";
import { base as baseExtensions } from "@rilldata/web-common/components/editor/presets/base";
import { DuckDBSQL } from "@rilldata/web-common/components/editor/presets/duckDBDialect";
import {
getStatementRange,
trimRange,
type SqlExecution,
} from "./query-utils";

let {
initialValue = "",
disabled = false,
onchange = (_value: string) => {},
onrun = (_execution: SqlExecution) => {},
}: {
initialValue?: string;
disabled?: boolean;
onchange?: (value: string) => void;
onrun?: (execution: SqlExecution) => void;
} = $props();

let parent: HTMLDivElement;
let editor: EditorView | null = null;

const setExecutedRange = StateEffect.define<
{ from: number; to: number } | undefined
>();
const executedRange = StateField.define<DecorationSet>({
create: () => Decoration.none,
update: (decorations, transaction) => {
decorations = decorations.map(transaction.changes);
for (const effect of transaction.effects) {
if (!effect.is(setExecutedRange)) continue;
decorations = effect.value
? Decoration.set([
Decoration.mark({ class: "cm-executed-statement" }).range(
effect.value.from,
effect.value.to,
),
])
: Decoration.none;
}
return decorations;
},
provide: (field) => EditorView.decorations.from(field),
});

onMount(() => {
editor = new EditorView({
state: EditorState.create({
doc: initialValue,
extensions: [
baseExtensions(),
sql({ dialect: DuckDBSQL }),
autocompletion({
override: [keywordCompletionSource(DuckDBSQL)],
icons: false,
}),
executedRange,
Prec.highest(
keymap.of([
{
key: "Mod-Enter",
run: () => {
runCurrent();
return true;
},
},
]),
),
EditorView.contentAttributes.of({
"aria-label": "SQL worksheet editor",
}),
EditorView.updateListener.of((update) => {
if (update.docChanged) onchange(update.state.doc.toString());
}),
],
}),
parent,
});

return () => editor?.destroy();
});

export function runCurrent() {
if (!editor || disabled) return;

const selection = editor.state.selection.main;
const document = editor.state.doc.toString();
const range = selection.empty
? getStatementRange(document, selection.head)
: trimRange(document, { from: selection.from, to: selection.to });
if (!range) return;

const endPosition = Math.max(range.from, range.to - 1);
editor.dispatch({
effects: setExecutedRange.of(range),
});
onrun({
...range,
sql: editor.state.sliceDoc(range.from, range.to),
startLine: editor.state.doc.lineAt(range.from).number,
endLine: editor.state.doc.lineAt(endPosition).number,
});
}
</script>

<div bind:this={parent} class="size-full overflow-hidden"></div>

<style lang="postcss">
div :global(.cm-editor) {
@apply h-full;
padding-top: 2px;
font-size: clamp(13px, calc(10px + 0.3vw), 15px);
}

div :global(.cm-scroller) {
@apply overflow-auto;
}

div :global(.cm-executed-statement) {
background-color: color-mix(
in oklab,
var(--color-primary-500) 10%,
transparent
);
}
</style>
Loading
Loading