Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,6 @@ playwright/.cache/
/.claude/settings.local.json

# Devcontainers - the lock file should be committed when more people use them
.devcontainer/devcontainer-lock.json
.devcontainer/devcontainer-lock.json
js-packages/support-bundle-triage/
.roc-check/
130 changes: 90 additions & 40 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion bunfig.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
[install]
# Only install package versions published at least 7 days ago
minimumReleaseAge = 604800
minimumReleaseAgeExcludes = ["svelte", "devalue"]
minimumReleaseAgeExcludes = []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed?

8 changes: 6 additions & 2 deletions crates/pipeline-manager/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use vergen_gitcl::*;

// These are touched during the build, so it would re-build every time if we
// don't exclude them from change detection:
const EXCLUDE_LIST: [&str; 10] = [
const EXCLUDE_LIST: [&str; 13] = [
"../../js-packages/web-console/node_modules",
"../../js-packages/web-console/build",
"../../js-packages/web-console/.svelte-kit",
Expand All @@ -17,15 +17,19 @@ const EXCLUDE_LIST: [&str; 10] = [
"../../js-packages/profiler-layout/.svelte-kit",
"../../js-packages/profiler-lib/node_modules",
"../../js-packages/profiler-lib/dist",
"../../js-packages/common-ui/node_modules",
"../../js-packages/common-ui/dist",
"../../js-packages/common-ui/.svelte-kit",
];

// Directories to include in change detection and build tracking:
const INCLUDE_LIST: [&str; 5] = [
const INCLUDE_LIST: [&str; 6] = [
"../../js-packages/web-console/",
"../../js-packages/profiler-app/",
"../../js-packages/profiler-layout/",
"../../js-packages/feldera-theme/",
"../../js-packages/profiler-lib/",
"../../js-packages/common-ui/",
];

/// The build script has two modes:
Expand Down
24 changes: 24 additions & 0 deletions js-packages/common-ui/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
node_modules

# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
/dist

# OS
.DS_Store
Thumbs.db

# Env
.env
.env.*
!.env.example
!.env.test

# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
1 change: 1 addition & 0 deletions js-packages/common-ui/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
engine-strict=true
51 changes: 51 additions & 0 deletions js-packages/common-ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# common-ui

Shared, app-agnostic Svelte UI building blocks used across Feldera's frontends
(the web console and the standalone profiler app). Anything reusable that isn't
tied to a specific app's routes or data model lives here, so the two apps stay
visually and behaviorally consistent without copy-pasting components.

The package is consumed as the `common-ui` workspace dependency; import from its
public entry point:

```ts
import { SegmentedControl, TabsPanel, PersistentContent, MonacoEditor } from 'common-ui'
```

## What's inside

- **Controls** — `SegmentedControl`, `Select`, `TabsPanel`, `Popover`, `Tooltip`.
- **Editor** — `MonacoEditorRunes` / `monaco.ts`: a runes-based Monaco wrapper.
- **Layout helpers** — `PersistentContent` + `persistentRect.svelte` for content
that must survive conditional re-renders (see those files for details),
`ANSIDecoratedText` for rendering ANSI-colored log output.

`src/lib` is the published surface (re-exported from `src/lib/index.ts`);
`src/routes` is only a local preview/showcase app and is not published.

## Developing

```sh
bun install
bun run dev # preview/showcase app
bun run check # type-check
```

## How it's built and consumed

Consumers import the **built** output (`dist/`), not the source. Building `web-console` or `profiler-app`
(`bun run build`) rebuilds this package automatically — their `prebuild` step
runs `build:deps:*` at the repo root, which includes
`bun --cwd=js-packages/common-ui run build`.

`bun run dev`, however, does **not** run that step, so it uses whatever is
already in `dist/`. After changing anything in `common-ui` while developing an
app, rebuild this package and restart the dev server:

```sh
bun --cwd=js-packages/common-ui run build # from the repo root
# then re-run `bun run dev` in web-console or profiler-app
```

This package was scaffolded with [`sv`](https://npmjs.com/package/sv); the
underlying tooling docs live at <https://svelte.dev/docs/kit/packaging>.
54 changes: 54 additions & 0 deletions js-packages/common-ui/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "common-ui",
"version": "0.0.1",
"devDependencies": {
"@monaco-editor/loader": "1.7.0",
"@skeletonlabs/skeleton": "^4.8.0",
"@skeletonlabs/skeleton-svelte": "^4.9.0",
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/kit": "^2.49.5",
"@sveltejs/package": "^2.5.7",
"@sveltejs/vite-plugin-svelte": "^7.0.0",
"@tailwindcss/vite": "^4.1.17",
"flowbite-svelte": "^1.31.0",
"monaco-editor": "0.55.1",
"publint": "^0.3.15",
"svelte": "5.55.7",
"svelte-check": "^4.3.5",
"tailwindcss": "^4.1.17",
"typescript": "^5.9.3",
"vite": "^8.0.0"
},
"peerDependencies": {
"@monaco-editor/loader": "1.7.0",
"@skeletonlabs/skeleton-svelte": "^4.9.0",
"fancy-ansi": "^0.1.3",
"flowbite-svelte": "^1.31.0",
"monaco-editor": "0.55.1",
"svelte": "5.55.7"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"svelte": "./dist/index.js"
}
},
"files": ["dist", "!dist/**/*.test.*", "!dist/**/*.spec.*"],
"keywords": ["svelte"],
"overrides": {
"devalue": "^5.6.2"
},
"scripts": {
"dev": "vite dev",
"build": "svelte-kit sync && vite build && bun run prepack",
"preview": "vite preview",
"prepare": "bun run prepack",
"prepack": "svelte-kit sync && svelte-package && publint",
"check": "svelte-kit sync && svelte-check --threshold error",
"check:watch": "svelte-kit sync && svelte-check --threshold error --watch"
},
"sideEffects": ["**/*.css"],
"svelte": "./dist/index.js",
"type": "module",
"types": "./dist/index.d.ts"
}
13 changes: 13 additions & 0 deletions js-packages/common-ui/src/app.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a helpful enumeration of framework interfaces, generated from the project template; it is my preference to leave it to quickly reference where I can extend an interface once it's need.

// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}

export {};
11 changes: 11 additions & 0 deletions js-packages/common-ui/src/app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!-- Render text decorated by ANSI escape sequences -->
<!-- Render text decorated by ANSI escape sequences. -->
<script lang="ts" module>
import { FancyAnsi } from 'fancy-ansi'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,17 @@
(a, b) => a.localeCompare(b)
)

import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'

self.MonacoEnvironment = {
getWorker(_: any, label: string) {
if (label === 'json') {
return new jsonWorker()
}
return new editorWorker()
}
}
// Worker setup is owned by `@feldera/vite-plugin-monaco-editor`: the plugin
// generates a `MonacoEnvironment.getWorker` whose worker imports honor its
// selected languages and `inlineWorkers` option. Consumers MUST configure
// that plugin in their Vite config; this component does not bundle workers.
</script>

<script lang="ts">
import type Monaco from 'monaco-editor'
import * as monacoImport from 'monaco-editor'
import * as monacoJson from 'monaco-editor/esm/vs/language/json/monaco.contribution.js'
import 'monaco-editor/esm/vs/language/json/jsonMode'
import { onDestroy, onMount } from 'svelte'
import loader from '@monaco-editor/loader'
import { felderaCompilerMarkerSource } from '$lib/functions/pipelines/monaco'
import felderaApiJsonSchemas from 'virtual:felderaApiJsonSchemas.json'

let monaco: typeof Monaco

Expand All @@ -45,6 +34,8 @@
model,
options,
markers,
markerSource,
jsonSchemas,
onready,
extras
}: {
Expand All @@ -55,6 +46,10 @@
'model' | 'value' | 'language'
>
markers?: Record<string, Monaco.editor.IMarkerData[]> | undefined
/** When `markers` is cleared, markers owned by this source are removed. */
markerSource?: string
/** Optional JSON schemas to register for `json`-language validation. */
jsonSchemas?: Monaco.json.DiagnosticsOptions['schemas']
onready: (event: Monaco.editor.IStandaloneCodeEditor) => void
extras?: {
isDarkMode?: boolean
Expand Down Expand Up @@ -84,7 +79,9 @@
return
}
if (!markers) {
monaco.editor.removeAllMarkers(felderaCompilerMarkerSource)
if (markerSource) {
monaco.editor.removeAllMarkers(markerSource)
}
return
}
setTimeout(() => {
Expand All @@ -95,14 +92,30 @@
})

onMount(async () => {
const jsonDefaults: Monaco.json.LanguageServiceDefaults = (monacoJson as any).jsonDefaults
if (!jsonDefaults.diagnosticsOptions.schemas?.length) {
jsonDefaults.setDiagnosticsOptions({
validate: true,
schemas: felderaApiJsonSchemas
})
}
monaco = await loader.init()
if (jsonSchemas) {
// `jsonDefaults` is a named export of the json `monaco.contribution`,
// NOT a property of the runtime `monaco` object — monaco only publishes
// the `editor` and `languages` namespaces on the aggregate. We dynamic-
// import it here; when the consumer enabled `'json'` in
// `@feldera/vite-plugin-monaco-editor`, this is a cache hit on the
// module that the plugin already statically loaded, and when they
// didn't, the plugin suppresses the import (the returned module is
// empty) and we warn instead of silently shipping a broken validator.
const { jsonDefaults } = (await import(
'monaco-editor/esm/vs/language/json/monaco.contribution.js'
)) as { jsonDefaults?: Monaco.json.LanguageServiceDefaults }
if (jsonDefaults && !jsonDefaults.diagnosticsOptions.schemas?.length) {
jsonDefaults.setDiagnosticsOptions({
validate: true,
schemas: jsonSchemas
})
} else if (!jsonDefaults) {
console.warn(
"[MonacoEditor] `jsonSchemas` was provided but the `json` language isn't enabled in @feldera/vite-plugin-monaco-editor — schemas ignored."
)
}
}
editor = monaco.editor.create(container, {
// TODO: Workaround for Windows-only cursor mis-positioning on mouse click
// (cursor lands progressively further off the clicked glyph along the line;
Expand Down Expand Up @@ -130,7 +143,7 @@
</script>

<div
class="monaco-container {options?.readOnly
class="h-full w-full p-0 m-0 {options?.readOnly
? extras?.isDarkMode
? 'monaco-readonly-dark'
: 'monaco-readonly'
Expand All @@ -141,24 +154,15 @@
<style>
.monaco-readonly {
:global(.sticky-line-content, .sticky-widget-line-numbers, .margin, .monaco-editor-background) {
/* @apply bg-surface-50; */
background-color: var(--color-surface-50);
}
}
.monaco-readonly-dark {
:global(.sticky-line-content, .sticky-widget-line-numbers, .margin, .monaco-editor-background) {
/* @apply bg-surface-950; */
background-color: var(--color-surface-950);
}
}

div.monaco-container {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}

div :global(.monaco-editor .monaco-inputbox .input) {
box-shadow: none;
}
Expand Down
47 changes: 47 additions & 0 deletions js-packages/common-ui/src/lib/PersistentContent.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!--
Renders content once, at the root of the layout, in a fixed-position overlay
that tracks the position and size of a placeholder elsewhere in the tree.

Use it for content that is expensive to (re-)initialise and must survive being
conditionally re-rendered — e.g. the profile viewer's dataflow diagram, whose
underlying graph library would otherwise lose its state every time the layout
toggles between the two panel arrangements. Instead of mounting the content
inside the conditional, mount it here once and drop a `<div use:handle.placeholder>`
wherever it should *appear*; this overlay follows that rect.

Pair with `usePersistentRect()`: pass its handle as `persistent` here, and
apply `handle.placeholder` to the placeholder div. See SupportBundleViewerLayout.svelte.
-->
<script lang="ts" module>
import type { Snippet } from 'svelte'
import type { PersistentHandle } from './persistentRect.svelte'

export type PersistentContentProps = {
/** Handle from `usePersistentRect()`. Pair this with the `placeholder` action
* applied to a `<div use:handle.placeholder>` elsewhere in the layout. */
persistent: PersistentHandle
/** Extra classes on the outer fixed-position wrapper (background, rounded corners,
* overflow clipping). Don't override `position: fixed` from here. */
class?: string
children: Snippet
}
</script>

<script lang="ts">
let { persistent, class: className = '', children }: PersistentContentProps = $props()
</script>

<!-- Always-mounted overlay. The fixed positioning is what lets it visually overlap the
placeholder's rect regardless of which conditional branch the placeholder is in.
`visibility: hidden` while the placeholder is unmounted or zero-sized; that still
preserves layout for any inner library (Cytoscape, etc.) measuring its own size. -->
<div
class="fixed {className}"
style:left="{persistent.rect.left}px"
style:top="{persistent.rect.top}px"
style:width="{persistent.rect.width}px"
style:height="{persistent.rect.height}px"
style:visibility={persistent.rect.visible ? 'visible' : 'hidden'}
>
{@render children()}
</div>
Loading
Loading