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: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Project Overview

**Stackable Cockpit** is a SvelteKit application that serves as the web UI of the Stackable Data Platform. It is built as a set of modules behind a shared OIDC login:
**Stackable Cockpit** is a SvelteKit application that serves as the UI of the Stackable Data Platform. It is built as a set of modules behind a shared OIDC login:

- **Trino** — SQL editor (Monaco + ANTLR grammar) with catalogue browser and query lifecycle management
- **Storage** — S3 file browser with preview, upload/download, archives, search and transfers (HDFS is a stub)
Expand Down Expand Up @@ -177,7 +177,7 @@ English is the default/fallback locale; German is also supported.

- All user-visible strings go into `messages/en.json` **and** `messages/de.json` (same keys in both). This includes `aria-label`, `title`, `placeholder` and `alt`.
- Use them via `import * as m from '$lib/paraglide/messages.js'` and `m.key_name()`.
- Flat keys with a feature prefix: `dashboard_welcome`, `nav_dashboard`, `header_user_menu`.
- Flat keys with a feature prefix: `dashboard_bookmarks_empty`, `nav_dashboard`, `header_user_menu`.
- Data structures containing translated strings must be `$derived` so they follow locale changes.
- The Vite plugin compiles messages during `dev`/`build`. For `npm run check`/`lint` outside Vite, compile manually: `npx @inlang/paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide`.

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

[Stackable Data Platform](https://stackable.tech/) | [Platform Docs](https://docs.stackable.tech/) | [Discussions](https://github.com/orgs/stackabletech/discussions) | [Discord](https://discord.gg/7kZ3BNnCAF)

Stackable Cockpit is the unified web UI for the Stackable Data Platform (SDP).
It provides a single place to work with the data services running on your platform.
Stackable Cockpit is the UI for the Stackable Data Platform (SDP).
It provides a single place to work with the services running on your platform.

It is part of the Stackable Data Platform, a curated selection of the best open source data apps like Apache Kafka®, OpenSearch, Trino or Apache Spark™, [all](#other-operators) working together seamlessly. Based on Kubernetes, it runs everywhere – on prem or in the cloud.

Expand Down
2 changes: 1 addition & 1 deletion deploy/helm/cockpit/Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
apiVersion: v2
name: cockpit
description: Unified data platform UI for Stackable's data infrastructure
description: Stackable Cockpit, the UI for the Stackable Data Platform (SDP)
type: application
version: 0.0.0-dev
appVersion: "0.0.0-dev"
Expand Down
6 changes: 3 additions & 3 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,9 @@ LABEL name="cockpit"
LABEL version="${VERSION}"
LABEL release="${VERSION}"
LABEL summary="Stackable Cockpit"
LABEL description="Web UI of the Stackable Data Platform"
LABEL description="UI of the Stackable Data Platform"
LABEL io.k8s.display-name="Stackable Cockpit"
LABEL io.k8s.description="Web UI of the Stackable Data Platform"
LABEL io.k8s.description="UI of the Stackable Data Platform"
LABEL io.openshift.tags="stackable,cockpit"
LABEL build-date="${BUILD_DATE}"
LABEL vcs-ref="${GIT_SHA}"
Expand All @@ -211,7 +211,7 @@ LABEL org.opencontainers.image.source="https://github.com/stackabletech/cockpit/
LABEL org.opencontainers.image.vendor="Stackable GmbH"
LABEL org.opencontainers.image.licenses="OSL-3.0"
LABEL org.opencontainers.image.title="Stackable Cockpit"
LABEL org.opencontainers.image.description="Web UI of the Stackable Data Platform"
LABEL org.opencontainers.image.description="UI of the Stackable Data Platform"
LABEL org.opencontainers.image.version="${VERSION}"
LABEL org.opencontainers.image.revision="${GIT_SHA}"
LABEL org.opencontainers.image.created="${BUILD_DATE}"
Expand Down
4 changes: 2 additions & 2 deletions docs/modules/cockpit/pages/index.adoc
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
= Stackable Cockpit
:description: Stackable Cockpit is a web UI for the Stackable Data Platform (SDP) to work with services such as Trino and S3 storage from a single place, behind one OIDC login.
:description: Stackable Cockpit is a UI for the Stackable Data Platform (SDP) to work with services such as Trino and S3 storage from a single place, behind one OIDC login.
:github: https://github.com/stackabletech/cockpit/

[.link-bar]
* {github}[GitHub {external-link-icon}^]

Stackable Cockpit is a web UI for the Stackable Data Platform (SDP).
Stackable Cockpit is a UI for the Stackable Data Platform (SDP).
Users log in once through OpenID Connect (OIDC) and work with the platform's services from a single place.

image::dashboard.png[Cockpit dashboard with bookmarks to Trino, Superset, Airflow and other tools]
Expand Down
2 changes: 1 addition & 1 deletion e2e/error-pages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ test.describe('Error pages', () => {
expect(response?.status()).toBe(404);
await waitForHydration(page);

await expect(page).toHaveTitle('Page not found | Stackable');
await expect(page).toHaveTitle('Page not found | Stackable Cockpit');
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Page not found');
const main = page.locator('main');
await expect(main.getByRole('heading', { name: /Page not found/ })).toBeVisible();
Expand Down
22 changes: 15 additions & 7 deletions e2e/i18n.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { test, expect } from './support/fixtures.js';
import fs from 'fs';
import { waitForHydration } from './support/helpers';

/** German dashboard heading (messages/de.json `dashboard_welcome`). */
const DE_DASHBOARD_HEADING = 'Stackable Unified Data Platform – Übersicht';
/** German bookmarks heading on the dashboard (messages/de.json `bookmark_section_title`). */
const DE_BOOKMARKS_HEADING = 'Lesezeichen';

/** Load the saved auth storage state, optionally stripping the locale cookie. */
function loadAuthState(authFile: string, { withoutLocale = false } = {}) {
Expand Down Expand Up @@ -33,7 +33,7 @@ test.describe('Internationalisation', () => {
await expect(html).toHaveAttribute('lang', 'en');

// Dashboard content is in English
await expect(page.getByText('Stackable Unified Data Platform overview')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Bookmarks', exact: true })).toBeVisible();
await expect(page.getByText('Add bookmark')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
Expand Down Expand Up @@ -68,7 +68,9 @@ test.describe('Internationalisation', () => {

// Page reloads with German content
await expect(page.locator('html')).toHaveAttribute('lang', 'de');
await expect(page.getByText(DE_DASHBOARD_HEADING)).toBeVisible();
await expect(
page.getByRole('heading', { name: DE_BOOKMARKS_HEADING, exact: true })
).toBeVisible();
await expect(page.getByText('Lesezeichen hinzufügen')).toBeVisible();
});

Expand All @@ -88,14 +90,18 @@ test.describe('Internationalisation', () => {

// Should render in German
await expect(page.locator('html')).toHaveAttribute('lang', 'de');
await expect(page.getByText(DE_DASHBOARD_HEADING)).toBeVisible();
await expect(
page.getByRole('heading', { name: DE_BOOKMARKS_HEADING, exact: true })
).toBeVisible();

// Navigate to same page (simulate navigation)
await page.goto('/');

// Should still be German
await expect(page.locator('html')).toHaveAttribute('lang', 'de');
await expect(page.getByText(DE_DASHBOARD_HEADING)).toBeVisible();
await expect(
page.getByRole('heading', { name: DE_BOOKMARKS_HEADING, exact: true })
).toBeVisible();
});

test('Accept-Language header respected for first visit', async ({
Expand All @@ -114,7 +120,9 @@ test.describe('Internationalisation', () => {

// Should render in German based on Accept-Language
await expect(page.locator('html')).toHaveAttribute('lang', 'de');
await expect(page.getByText(DE_DASHBOARD_HEADING)).toBeVisible();
await expect(
page.getByRole('heading', { name: DE_BOOKMARKS_HEADING, exact: true })
).toBeVisible();

await context.close();
});
Expand Down
4 changes: 2 additions & 2 deletions e2e/navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@ test.describe('App navigation', () => {

const sidebar = page.getByRole('complementary', { name: 'Sidebar' });
// Expanded: the visible "Stackable" text names the brand, so the logo is decorative.
await expect(sidebar.getByRole('img', { name: 'Stackable' })).toHaveCount(0);
await expect(sidebar.getByRole('img', { name: 'Stackable Cockpit' })).toHaveCount(0);

await sidebar.getByRole('button', { name: 'Collapse sidebar' }).click();
await expect(sidebar.getByRole('img', { name: 'Stackable' })).toBeVisible();
await expect(sidebar.getByRole('img', { name: 'Stackable Cockpit' })).toBeVisible();
});

test('skip link is the first tab stop and moves keyboard focus into the main content', async ({
Expand Down
4 changes: 2 additions & 2 deletions e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ test.describe('Smoke tests', () => {
await waitForHydration(page);

// Sidebar brand is visible
await expect(page.getByText('Stackable', { exact: true })).toBeVisible();
await expect(page.getByText('Stackable Cockpit', { exact: true })).toBeVisible();

// Dashboard nav item is present and active
const dashboardLink = page.getByRole('link', { name: 'Dashboard' });
Expand All @@ -26,7 +26,7 @@ test.describe('Smoke tests', () => {
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();

// Dashboard content is rendered
await expect(page.getByText('Stackable Unified Data Platform overview')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Bookmarks', exact: true })).toBeVisible();

// Trino nav item is present and navigable
const trinoLink = page.getByRole('link', { name: 'Trino' });
Expand Down
6 changes: 2 additions & 4 deletions messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
"$schema": "https://inlang.com/schema/inlang-message-format",
"action_close": "Schließen",
"action_dismiss": "Schließen",
"dashboard_welcome": "Stackable Unified Data Platform – Übersicht",
"dashboard_subtitle": "Die Werkzeuge, die Sie sich als Lesezeichen gespeichert haben. Lesezeichen werden von Hand hinzugefügt – Cockpit erkennt noch nichts automatisch.",
"dashboard_bookmarks_empty": "Noch keine Lesezeichen. Fügen Sie Ihr erstes Lesezeichen hinzu, um zu starten.",
"nav_platform": "Plattform",
"nav_dashboard": "Dashboard",
Expand Down Expand Up @@ -38,8 +36,8 @@
"theme_toggle": "Design umschalten",
"page_title_dashboard": "Dashboard",
"page_title_default": "Stackable",
"page_title_suffix": "Stackable",
"meta_description": "Stackable Cockpit: Die Web-UI für die Stackable Data Platform (SDP). Arbeiten Sie an einem zentralen Ort mit den Datendiensten der Plattform.",
"app_name": "Stackable Cockpit",
"meta_description": "Stackable Cockpit: Die UI für die Stackable Data Platform (SDP). Arbeiten Sie an einem zentralen Ort mit den Diensten der Plattform.",
"page_title_trino": "Trino",
"bookmark_count_links": [
{
Expand Down
6 changes: 2 additions & 4 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
"$schema": "https://inlang.com/schema/inlang-message-format",
"action_close": "Close",
"action_dismiss": "Dismiss",
"dashboard_welcome": "Stackable Unified Data Platform overview",
"dashboard_subtitle": "The tools you've bookmarked. Bookmarks are added by hand — Cockpit doesn't discover anything yet.",
"dashboard_bookmarks_empty": "No bookmarks yet. Add your first bookmark to get started.",
"nav_platform": "Platform",
"nav_dashboard": "Dashboard",
Expand Down Expand Up @@ -38,8 +36,8 @@
"theme_toggle": "Toggle theme",
"page_title_dashboard": "Dashboard",
"page_title_default": "Stackable",
"page_title_suffix": "Stackable",
"meta_description": "Stackable Cockpit: The web UI for the Stackable Data Platform (SDP). Work with the data services running on your platform in one place.",
"app_name": "Stackable Cockpit",
"meta_description": "Stackable Cockpit: The UI for the Stackable Data Platform (SDP). Work with the services running on your platform in one place.",
"page_title_trino": "Trino",
"bookmark_count_links": [
{
Expand Down
7 changes: 2 additions & 5 deletions src/lib/components/dashboard/DashboardBookmarks.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -59,21 +59,18 @@
<div class="mb-2.5 flex flex-wrap items-end justify-between gap-4">
<div class="min-w-0">
<div class="flex items-baseline gap-2.5">
<h3
<h2
id="bookmarks-heading"
class="text-base-content/70 text-xs font-bold tracking-wider uppercase"
>
{m.bookmark_section_title()}
</h3>
</h2>
{#if count > 0}
<span class="text-base-content/70 text-xs">
{m.bookmark_count_links({ count })}
</span>
{/if}
</div>
{#if bookmarks}
<p class="text-base-content/70 mt-1.5 max-w-xl text-sm">{m.dashboard_subtitle()}</p>
{/if}
</div>
{#if bookmarks}
<button type="button" class="btn btn-primary btn-sm rounded-full" onclick={openAddBookmark}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,20 @@ const globalBookmark: BookmarkLink = {
describe('DashboardBookmarks', () => {
beforeEach(() => vi.clearAllMocks());

it('renders the section heading, subtitle and Add bookmark button', async () => {
it('renders the section heading and Add bookmark button', async () => {
render(DashboardBookmarks, { bookmarks: [] });

await expect
.element(page.getByRole('heading', { level: 3, name: 'Bookmarks' }))
.element(page.getByRole('heading', { level: 2, name: 'Bookmarks' }))
.toBeInTheDocument();
await expect.element(page.getByText(/bookmarks are added by hand/i)).toBeInTheDocument();
await expect.element(page.getByRole('button', { name: /add bookmark/i })).toBeInTheDocument();
});

it('shows a message instead of the list, without adding, when loading failed', async () => {
render(DashboardBookmarks, { bookmarks: null, loadFailed: true });

await expect
.element(page.getByRole('heading', { level: 3, name: 'Bookmarks' }))
.element(page.getByRole('heading', { level: 2, name: 'Bookmarks' }))
.toBeInTheDocument();
await expect
.element(page.getByText('Bookmarks could not be loaded. Please reload the page later.'))
Expand Down Expand Up @@ -157,7 +156,6 @@ describe('DashboardBookmarks', () => {
await expect
.element(page.getByRole('button', { name: /add bookmark/i }))
.not.toBeInTheDocument();
await expect.element(page.getByText(/bookmarks are added by hand/i)).not.toBeInTheDocument();
await expect.element(page.getByText(/no bookmarks yet/i)).not.toBeInTheDocument();
});

Expand Down
4 changes: 2 additions & 2 deletions src/lib/components/layout/sidebar/Sidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -212,11 +212,11 @@
<div class="border-base-300 flex h-16 shrink-0 items-center gap-3 border-b px-4">
<enhanced:img
src={stackableLogo}
alt={collapsed ? m.page_title_default() : ''}
alt={collapsed ? m.app_name() : ''}
class="h-7 w-7 shrink-0"
/>
{#if !collapsed}
<span class="text-base-content text-lg font-bold tracking-tight">Stackable</span>
<span class="text-base-content text-lg font-bold tracking-tight">{m.app_name()}</span>
{/if}
</div>

Expand Down
2 changes: 1 addition & 1 deletion src/routes/(app)/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
</script>

<svelte:head>
<title>{title} | {m.page_title_suffix()}</title>
<title>{title} | {m.app_name()}</title>
</svelte:head>

<div class="bg-base-100 flex h-dvh overflow-hidden">
Expand Down
3 changes: 0 additions & 3 deletions src/routes/(app)/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
<script lang="ts">
import * as m from '$lib/paraglide/messages.js';
import DashboardBookmarks from '$lib/components/dashboard/DashboardBookmarks.svelte';

let { data } = $props();
</script>

<div class="mx-auto max-w-6xl space-y-5">
<h2 class="text-base-content text-2xl font-bold tracking-tight">{m.dashboard_welcome()}</h2>

<!-- Without a signed-in user only global bookmarks are shown, and without any
the section is hidden; when loading the user's bookmarks failed, it says so. -->
{#if data.bookmarks || data.bookmarksFailed || data.globalBookmarks.length > 0}
Expand Down
9 changes: 1 addition & 8 deletions src/routes/(app)/page.svelte.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,10 @@ const renderPage = (
} as unknown as Parameters<typeof render<typeof Page>>[1]);

describe('/(app)/+page.svelte', () => {
it('should render the platform heading', async () => {
renderPage();
await expect.element(page.getByRole('heading', { level: 2 })).toBeInTheDocument();
});

it('should render the bookmarks section', async () => {
renderPage();
await expect
.element(page.getByRole('heading', { level: 3, name: 'Bookmarks' }))
.element(page.getByRole('heading', { level: 2, name: 'Bookmarks' }))
.toBeInTheDocument();
});

Expand All @@ -63,11 +58,9 @@ describe('/(app)/+page.svelte', () => {
it('hides everything bookmark-related without a user or global bookmarks', async () => {
renderPage(null);

await expect.element(page.getByRole('heading', { level: 2 })).toBeInTheDocument();
await expect.element(page.getByRole('heading', { name: 'Bookmarks' })).not.toBeInTheDocument();
await expect
.element(page.getByRole('button', { name: /add bookmark/i }))
.not.toBeInTheDocument();
await expect.element(page.getByText(/bookmarks are added by hand/i)).not.toBeInTheDocument();
});
});
2 changes: 1 addition & 1 deletion src/routes/auth/login/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
</script>

<svelte:head>
<title>{m.auth_login_title()} | {m.page_title_suffix()}</title>
<title>{m.auth_login_title()} | {m.app_name()}</title>
</svelte:head>

<div class="bg-base-200 flex min-h-dvh items-center justify-center p-4">
Expand Down
Loading