diff --git a/docs/blog/0.52.md b/docs/blog/0.52.md new file mode 100644 index 00000000000..66282ec2f6c --- /dev/null +++ b/docs/blog/0.52.md @@ -0,0 +1,62 @@ +--- + +date: 2024-12-12 +image: https://github.com/rilldata/rill/assets/5587788/b30486f6-002a-445d-8a1b-955b6ec0066d + +--- + +# Rill 0.52 - Visual Explore Editing, human readable URLs, env variables in the cloud and breadcrumbs! + +:::note +⚡ Rill Developer is a tool that makes it effortless to transform your datasets with SQL and create fast, exploratory dashboards. Rill Cloud can then help to enable shared collaboration at scale. + +To [try out Rill Developer, check out these instructions](/home/install) and [join us over on Discord](https://bit.ly/3bbcSl9) to meet the team behind the product as well as other users. In addition, once you have a project set up in Rill Developer, you can then [deploy the project](/deploy/deploy-dashboard) and collaborate with others / invite your team members by [logging into Rill Cloud](https://ui.rilldata.com)! +::: + +![release-0 52]() + +## Visual Explore Editing +With our 0.51 release we introduced the ability to visually edit metrics and dimensions in the metrics view. Now we are extending the same capabilities also for Explore dashboards. So if you prefer a UX experience over editing yaml files directly then this is for you! + +## Human Readable URLs +As you navigate around inside of a Rill dashboard what you see on the screen is always present in the URL so that you can easily share that URL with other users and they will see exactly what you are seeing on the screen. Previously we encoded this URL so that it was very hard to read, change or make updates to but with our 0.52 release the URL will now be fully human readable. This allows you to more easily embed a Rill dashboard into intranets and dynamically change what the user sees on the screen and when you get sent a link from someone it's easier to understand what the actual URL will show on the screen. + +## Environment Variables - Manage in Rill Cloud +In Rill Cloud you can now visually manage your environment variables for a project. Add, update or delete them directly from Rill Cloud without having to fire up the Rill CLI. Change a password env variable and all your developers have to do is a rill env pull and they are up-to-date! + +## Project Breadcrumbs in Rill Developer +As you are navigating around in Rill Developer you will now find breadcrumbs that shows you the associated resources so that you can quickly navigate between them as you are making changes. An early quality of life holiday present! + +## Bug Fixes and Misc +- Added support for STS AssumeRole in Athena connector. +- Support locate formatting using `d3_locale` in metrics view. +- Added UI for Environment Variables in Rill Cloud. +- Removed "Back to Home" from Embed Dashboards. +- Improved Big number KPI formatting rules. +- Default to UTC when no timezones are selected. +- Environmental Variables are now available in security policies. +- Port not required when connecting to Cloud OLAP URL. +- Improved UI: Improved project user list UI. +- Improved UI: Improved buttons to return to Explore main page. +- Improved UX: Disable slack notification by default. +- Improved UX: Refresh button should initiate incremental refresh where possible. +- Improved UX: Support infinite query when listing org users and invites. +- Improved UX: Offer to copy to clipboard CTA when creating public URL +- Improved UX: Users able to drag and select multiple values to copy in pivot. +- Improved UX: Add clear button on pivot header for rows and columns. +- Improved UX: Added error handling for row access policies. +- fixed Leaderboard column sorting behavior. +- fixed `Select all` button in the dimension leaderboard. +- fixed Scroll position in pivot table when loading new rows. +- fixed environmental overrides in rill.yaml. +- fixed custom compare time ranges in reports or alerts. +- fixed leaderboard header no label issue, defaults to name. +- fixed case issues with bookmarks. +- fixed autogenerated metric view indentation and default parameters. +- fixed issue when switching dashboards based on the same metrics view in Rill Cloud. +- fixed formatting in TDD when formatting is not defined, use humanize. +- fixed issue in embed dashboard when pivot disabled, `Start pivot` still visible. +- fixed issue where could not search by name in Org search. +- fixed issue where limit was not being applied to exports. +- fixed issue when disabling compare dimension in TDD not being respected. +- fixed issue where ClickHouse would auto unnest in metrics view. diff --git a/package-lock.json b/package-lock.json index 94b713aeb25..59f1a5cb0b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2698,6 +2698,20 @@ "crelt": "^1.0.5" } }, + "node_modules/@codemirror/merge": { + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@codemirror/merge/-/merge-6.7.4.tgz", + "integrity": "sha512-9FpIFTgzkaxkZE93XKoFR6caAB6sCAfYCW2NT+atGEmdv/1Mt1ouxA+hKxGRYdMvdH9Ph0KMJtYnzEi+QCGAiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/highlight": "^1.0.0", + "style-mod": "^4.1.0" + } + }, "node_modules/@codemirror/search": { "version": "6.5.6", "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz", @@ -35255,6 +35269,7 @@ "@codemirror/lang-yaml": "^6.1.1", "@codemirror/language": "^6.10.3", "@codemirror/lint": "^6.8.2", + "@codemirror/merge": "^6.7.4", "@codemirror/search": "^6.5.6", "@codemirror/state": "^6.4.1", "@codemirror/view": "^6.34.1", diff --git a/runtime/compilers/rillv1/parse_node.go b/runtime/compilers/rillv1/parse_node.go index 8beea576f6d..0b6d0d973da 100644 --- a/runtime/compilers/rillv1/parse_node.go +++ b/runtime/compilers/rillv1/parse_node.go @@ -73,6 +73,9 @@ type commonYAML struct { Kind *string `yaml:"kind"` // Name is usually inferred from the filename, but can be specified manually. Name string `yaml:"name"` + // Namespace is an optional value to group resources by. + // It currently just gets pre-pended to the resource name in the format `/`. + Namespace string `yaml:"namespace"` // Refs are a list of other resources that this resource depends on. They are usually inferred from other fields, but can also be specified manually. Refs []yaml.Node `yaml:"refs"` // ParserConfig enables setting file-level parser config. @@ -283,6 +286,11 @@ func (p *Parser) parseStem(paths []string, ymlPath, yml, sqlPath, sql string) (* } } + // If a namespace was provided in YAML, prepend it to the name. + if cfg != nil && cfg.Namespace != "" { + res.Name = cfg.Namespace + ":" + res.Name + } + // If resource kind is not set in YAML or SQL, try to infer it from the context if res.Kind == ResourceKindUnspecified { if strings.HasPrefix(paths[0], "/sources") { diff --git a/runtime/compilers/rillv1/parser_test.go b/runtime/compilers/rillv1/parser_test.go index bbfaecd942b..71e4aea2b6b 100644 --- a/runtime/compilers/rillv1/parser_test.go +++ b/runtime/compilers/rillv1/parser_test.go @@ -2041,6 +2041,50 @@ refresh: requireResourcesAndErrors(t, p, []*Resource{m1, m2}, nil) } +func TestNamespace(t *testing.T) { + ctx := context.Background() + repo := makeRepo(t, map[string]string{ + `rill.yaml`: ``, + `models/m1.yaml`: ` +type: model +sql: SELECT 1 +`, + `explores/e1.yaml`: ` +type: explore +namespace: foo +metrics_view: missing +`, + }) + + resources := []*Resource{ + { + Name: ResourceName{Kind: ResourceKindModel, Name: "m1"}, + Paths: []string{"/models/m1.yaml"}, + ModelSpec: &runtimev1.ModelSpec{ + RefreshSchedule: &runtimev1.Schedule{RefUpdate: true}, + InputConnector: "duckdb", + InputProperties: must(structpb.NewStruct(map[string]any{"sql": `SELECT 1`})), + OutputConnector: "duckdb", + }, + }, + { + Name: ResourceName{Kind: ResourceKindExplore, Name: "foo:e1"}, + Paths: []string{"/explores/e1.yaml"}, + Refs: []ResourceName{{Kind: ResourceKindMetricsView, Name: "missing"}}, + ExploreSpec: &runtimev1.ExploreSpec{ + DisplayName: "Foo: E1", + MetricsView: "missing", + DimensionsSelector: &runtimev1.FieldSelector{Selector: &runtimev1.FieldSelector_All{All: true}}, + MeasuresSelector: &runtimev1.FieldSelector{Selector: &runtimev1.FieldSelector_All{All: true}}, + }, + }, + } + + p, err := Parse(ctx, repo, "", "", "duckdb") + require.NoError(t, err) + requireResourcesAndErrors(t, p, resources, nil) +} + func requireResourcesAndErrors(t testing.TB, p *Parser, wantResources []*Resource, wantErrors []*runtimev1.ParseError) { // Check errors // NOTE: Assumes there's at most one parse error per file path diff --git a/runtime/compilers/rillv1/util.go b/runtime/compilers/rillv1/util.go index 150c56056cc..4d98bdafe6e 100644 --- a/runtime/compilers/rillv1/util.go +++ b/runtime/compilers/rillv1/util.go @@ -18,6 +18,9 @@ func ToDisplayName(name string) string { name = strings.ReplaceAll(name, "_", " ") name = strings.ReplaceAll(name, "-", " ") + // Replace colons with colon-space. + name = strings.ReplaceAll(name, ":", ": ") + // Capitalize the first letter. name = cases.Title(language.English).String(name) diff --git a/web-admin/src/features/bookmarks/BookmarksDropdownMenuContent.svelte b/web-admin/src/features/bookmarks/BookmarksDropdownMenuContent.svelte index e0cfbe739a5..7d738ab2a65 100644 --- a/web-admin/src/features/bookmarks/BookmarksDropdownMenuContent.svelte +++ b/web-admin/src/features/bookmarks/BookmarksDropdownMenuContent.svelte @@ -80,6 +80,7 @@ $schemaResp.data?.schema, $exploreState, defaultExplorePreset, + $metricsViewTimeRange.data?.timeRangeSummary, ); $: filteredBookmarks = searchBookmarks(categorizedBookmarks, searchText); diff --git a/web-admin/src/features/bookmarks/selectors.ts b/web-admin/src/features/bookmarks/selectors.ts index d870486ad62..cdba5a8f2ba 100644 --- a/web-admin/src/features/bookmarks/selectors.ts +++ b/web-admin/src/features/bookmarks/selectors.ts @@ -8,7 +8,10 @@ import { getDashboardStateFromUrl } from "@rilldata/web-common/features/dashboar import { useMetricsViewTimeRange } from "@rilldata/web-common/features/dashboards/selectors"; import { useExploreState } from "@rilldata/web-common/features/dashboards/stores/dashboard-stores"; import type { MetricsExplorerEntity } from "@rilldata/web-common/features/dashboards/stores/metrics-explorer-entity"; -import { timeControlStateSelector } from "@rilldata/web-common/features/dashboards/time-controls/time-control-store"; +import { + getTimeControlState, + timeControlStateSelector, +} from "@rilldata/web-common/features/dashboards/time-controls/time-control-store"; import { convertExploreStateToURLSearchParams } from "@rilldata/web-common/features/dashboards/url-state/convertExploreStateToURLSearchParams"; import { ResourceKind } from "@rilldata/web-common/features/entity-management/resource-selectors"; import { useExploreValidSpec } from "@rilldata/web-common/features/explores/selectors"; @@ -20,6 +23,7 @@ import { type V1ExploreSpec, type V1MetricsViewSpec, type V1StructType, + type V1TimeRangeSummary, } from "@rilldata/web-common/runtime-client"; import type { QueryClient } from "@tanstack/query-core"; import { derived, get, type Readable } from "svelte/store"; @@ -61,6 +65,7 @@ export function categorizeBookmarks( schema: V1StructType | undefined, exploreState: MetricsExplorerEntity, defaultExplorePreset: V1ExplorePreset, + timeRangeSummary: V1TimeRangeSummary | undefined, ) { const bookmarks: Bookmarks = { home: undefined, @@ -76,6 +81,7 @@ export function categorizeBookmarks( schema ?? {}, exploreState, defaultExplorePreset, + timeRangeSummary, ); if (isHomeBookmark(bookmarkResource)) { bookmarks.home = bookmark; @@ -143,6 +149,7 @@ export function convertBookmarkToUrlSearchParams( schema: V1StructType, exploreState: MetricsExplorerEntity | undefined, defaultExplorePreset: V1ExplorePreset, + timeRangeSummary: V1TimeRangeSummary | undefined, ) { const exploreStateFromBookmark = getDashboardStateFromUrl( bookmarkResource.data ?? "", @@ -150,12 +157,19 @@ export function convertBookmarkToUrlSearchParams( exploreSpec, schema, ); + const finalExploreState = { + ...(exploreState ?? {}), + ...exploreStateFromBookmark, + } as MetricsExplorerEntity; return convertExploreStateToURLSearchParams( - { - ...(exploreState ?? {}), - ...exploreStateFromBookmark, - } as MetricsExplorerEntity, + finalExploreState, exploreSpec, + getTimeControlState( + metricsViewSpec, + exploreSpec, + timeRangeSummary, + finalExploreState, + ), defaultExplorePreset, ); } @@ -167,6 +181,7 @@ function parseBookmark( schema: V1StructType, exploreState: MetricsExplorerEntity, defaultExplorePreset: V1ExplorePreset, + timeRangeSummary: V1TimeRangeSummary | undefined, ): BookmarkEntry { const url = new URL(get(page).url); url.search = convertBookmarkToUrlSearchParams( @@ -176,6 +191,7 @@ function parseBookmark( schema, exploreState, defaultExplorePreset, + timeRangeSummary, ); return { resource: bookmarkResource, diff --git a/web-admin/src/features/dashboards/query-mappers/utils.ts b/web-admin/src/features/dashboards/query-mappers/utils.ts index 9168f2bb090..f2c14056076 100644 --- a/web-admin/src/features/dashboards/query-mappers/utils.ts +++ b/web-admin/src/features/dashboards/query-mappers/utils.ts @@ -1,5 +1,6 @@ import { createInExpression } from "@rilldata/web-common/features/dashboards/stores/filter-utils"; import type { MetricsExplorerEntity } from "@rilldata/web-common/features/dashboards/stores/metrics-explorer-entity"; +import { getTimeControlState } from "@rilldata/web-common/features/dashboards/time-controls/time-control-store"; import { PreviousCompleteRangeMap } from "@rilldata/web-common/features/dashboards/time-controls/time-range-mappers"; import { convertExploreStateToURLSearchParams } from "@rilldata/web-common/features/dashboards/url-state/convertExploreStateToURLSearchParams"; import { getDefaultExplorePreset } from "@rilldata/web-common/features/dashboards/url-state/getDefaultExplorePreset"; @@ -182,6 +183,7 @@ export async function getExplorePageUrl( const url = new URL(`${curPageUrl.protocol}//${curPageUrl.host}`); url.pathname = `/${organization}/${project}/explore/${exploreName}`; + const metricsViewSpec = metricsView?.metricsView?.state?.validSpec ?? {}; const exploreSpec = explore?.explore?.state?.validSpec ?? {}; const metricsViewName = exploreSpec.metricsView; @@ -206,6 +208,12 @@ export async function getExplorePageUrl( url.search = convertExploreStateToURLSearchParams( exploreState, exploreSpec, + getTimeControlState( + metricsViewSpec, + exploreSpec, + fullTimeRange?.timeRangeSummary, + exploreState, + ), getDefaultExplorePreset(exploreSpec, fullTimeRange), ); return url.toString(); diff --git a/web-admin/src/features/projects/status/ProjectGlobalStatusIndicator.svelte b/web-admin/src/features/projects/status/ProjectGlobalStatusIndicator.svelte index 6c6b34569f0..bc0a718d4f5 100644 --- a/web-admin/src/features/projects/status/ProjectGlobalStatusIndicator.svelte +++ b/web-admin/src/features/projects/status/ProjectGlobalStatusIndicator.svelte @@ -30,12 +30,17 @@ .length > 0 ); }, + refetchOnMount: true, + refetchOnWindowFocus: true, }, }, ); $: hasResourceErrors = $hasResourceErrorsQuery.data; - $: projectParserQuery = useProjectParser(queryClient, instanceId); + $: projectParserQuery = useProjectParser(queryClient, instanceId, { + refetchOnMount: true, + refetchOnWindowFocus: true, + }); $: hasParseErrors = $projectParserQuery?.data?.projectParser.state.parseErrors.length > 0; diff --git a/web-admin/src/features/projects/status/ProjectParseErrors.svelte b/web-admin/src/features/projects/status/ProjectParseErrors.svelte index eb1ec2ad4e8..ff32404d884 100644 --- a/web-admin/src/features/projects/status/ProjectParseErrors.svelte +++ b/web-admin/src/features/projects/status/ProjectParseErrors.svelte @@ -8,10 +8,21 @@ import { createRuntimeServiceGetResource } from "@rilldata/web-common/runtime-client"; import { runtime } from "@rilldata/web-common/runtime-client/runtime-store"; - $: projectParserQuery = createRuntimeServiceGetResource($runtime.instanceId, { - "name.kind": ResourceKind.ProjectParser, - "name.name": SingletonProjectParserName, - }); + $: ({ instanceId } = $runtime); + + $: projectParserQuery = createRuntimeServiceGetResource( + instanceId, + { + "name.kind": ResourceKind.ProjectParser, + "name.name": SingletonProjectParserName, + }, + { + query: { + refetchOnMount: true, + refetchOnWindowFocus: true, + }, + }, + ); $: ({ isLoading, isSuccess, data, error } = $projectParserQuery); $: parseErrors = data?.resource?.projectParser.state.parseErrors; @@ -30,7 +41,7 @@ {:else if isSuccess} {#if parseErrors && parseErrors.length > 0}
    - {#each parseErrors as error} + {#each parseErrors as error (error.message)}
  • diff --git a/web-admin/src/features/projects/status/ProjectResources.svelte b/web-admin/src/features/projects/status/ProjectResources.svelte index 68e91debcbb..01e468c41c5 100644 --- a/web-admin/src/features/projects/status/ProjectResources.svelte +++ b/web-admin/src/features/projects/status/ProjectResources.svelte @@ -16,8 +16,10 @@ let isReconciling = false; + $: ({ instanceId } = $runtime); + $: resources = createRuntimeServiceListResources( - $runtime.instanceId, + instanceId, // All resource "kinds" undefined, { @@ -30,6 +32,7 @@ ); }, refetchOnMount: true, + refetchOnWindowFocus: true, refetchInterval: isReconciling ? 500 : false, }, }, diff --git a/web-common/package.json b/web-common/package.json index 7a79afe5ea7..cf756eca95c 100644 --- a/web-common/package.json +++ b/web-common/package.json @@ -25,6 +25,7 @@ "@codemirror/lang-yaml": "^6.1.1", "@codemirror/language": "^6.10.3", "@codemirror/lint": "^6.8.2", + "@codemirror/merge": "^6.7.4", "@codemirror/search": "^6.5.6", "@codemirror/state": "^6.4.1", "@codemirror/view": "^6.34.1", diff --git a/web-common/src/components/button/Button.svelte b/web-common/src/components/button/Button.svelte index 73bd65ab701..d6f01fab12b 100644 --- a/web-common/src/components/button/Button.svelte +++ b/web-common/src/components/button/Button.svelte @@ -1,6 +1,7 @@ + + + + + + + diff --git a/web-common/src/features/dashboards/stores/dashboard-stores.ts b/web-common/src/features/dashboards/stores/dashboard-stores.ts index 57a4b78f06b..51ef6a4168b 100644 --- a/web-common/src/features/dashboards/stores/dashboard-stores.ts +++ b/web-common/src/features/dashboards/stores/dashboard-stores.ts @@ -168,8 +168,6 @@ function syncDimensions( const metricsViewReducers = { init(name: string, initState: Partial = {}) { update((state) => { - if (state.entities[name]) return state; - state.entities[name] = getFullInitExploreState(name, initState); updateMetricsExplorerProto(state.entities[name]); diff --git a/web-common/src/features/dashboards/stores/test-data/data.ts b/web-common/src/features/dashboards/stores/test-data/data.ts index 18a707952ed..79a329a1d99 100644 --- a/web-common/src/features/dashboards/stores/test-data/data.ts +++ b/web-common/src/features/dashboards/stores/test-data/data.ts @@ -193,6 +193,7 @@ export const AD_BIDS_METRICS_3_MEASURES_DIMENSIONS: V1MetricsViewSpec = { table: AD_BIDS_SOURCE_NAME, measures: AD_BIDS_THREE_MEASURES, dimensions: AD_BIDS_THREE_DIMENSIONS, + timeDimension: AD_BIDS_TIMESTAMP_DIMENSION, }; export const AD_BIDS_EXPLORE_INIT: V1ExploreSpec = { diff --git a/web-common/src/features/dashboards/time-controls/super-pill/components/Comparison.svelte b/web-common/src/features/dashboards/time-controls/super-pill/components/Comparison.svelte index 59eb3e0783b..aa70a0d862a 100644 --- a/web-common/src/features/dashboards/time-controls/super-pill/components/Comparison.svelte +++ b/web-common/src/features/dashboards/time-controls/super-pill/components/Comparison.svelte @@ -96,9 +96,10 @@ }} typeahead={!showSelector} > - + - - - - - - - - + diff --git a/web-common/src/features/editor/DiffBar.svelte b/web-common/src/features/editor/DiffBar.svelte new file mode 100644 index 00000000000..8aa32f564aa --- /dev/null +++ b/web-common/src/features/editor/DiffBar.svelte @@ -0,0 +1,46 @@ + + +
    +
    +

    Unsaved changes

    + + +
    + +
    +

    Incoming content

    + +
    +
    + + diff --git a/web-common/src/features/editor/Editor.svelte b/web-common/src/features/editor/Editor.svelte index c6f44922f6b..7335d4d12be 100644 --- a/web-common/src/features/editor/Editor.svelte +++ b/web-common/src/features/editor/Editor.svelte @@ -15,12 +15,14 @@ import TooltipShortcutContainer from "@rilldata/web-common/components/tooltip/TooltipShortcutContainer.svelte"; import Shortcut from "@rilldata/web-common/components/tooltip/Shortcut.svelte"; import MetaKey from "@rilldata/web-common/components/tooltip/MetaKey.svelte"; + import * as AlertDialog from "@rilldata/web-common/components/alert-dialog/"; + import Alert from "@rilldata/web-common/components/icons/Alert.svelte"; + import DiffBar from "./DiffBar.svelte"; export let fileArtifact: FileArtifact; export let extensions: Extension[] = []; export let autoSave = true; export let editor: EditorView; - export let forceLocalUpdates = false; export let forceDisableAutoSave = false; export let showSaveBar = true; export let refetchOnWindowFocus = true; @@ -28,31 +30,38 @@ export let onRevert: () => void = () => {}; $: ({ - hasUnsavedChanges, saveLocalContent, - revert, - localContent, + revertChanges, + merging, + editorContent, disableAutoSave, + inConflict, + saveState: { saving, error, resolve }, + saveEnabled, } = fileArtifact); $: debounceSave = debounce(save, FILE_SAVE_DEBOUNCE_TIME); + $: disabled = !$saveEnabled; + async function handleKeydown(e: KeyboardEvent) { if (e.key === "s" && (e.ctrlKey || e.metaKey)) { e.preventDefault(); + if (disabled) return; await save(); } } - async function save() { - const local = $localContent; + async function save(force = false) { + const local = $editorContent; if (local === null) return; onSave(local); - await saveLocalContent(); + await saveLocalContent(force); } function revertContent() { - revert(); // Revert fileArtifact to remote content + revertChanges(); // Revert fileArtifact to remote content + resolve(); onRevert(); // Call revert callback } @@ -64,12 +73,19 @@
    + {#if $merging} + save(true)} + onAcceptIncoming={revertContent} + /> + {/if} +
    {#key fileArtifact} - {#if showSaveBar} + {#if !$merging && showSaveBar}
    {#if !autoSave || disableAutoSave || forceDisableAutoSave} @@ -100,11 +128,7 @@ - @@ -128,9 +152,36 @@ {/if}
    +{#if $inConflict && !$merging} + + + File update detected + + This file has been modified by another application. Please resolve + conflicts with your unsaved changes before proceeding. + + + + + + + + + +{/if} +