,
body: (
<>
diff --git a/frontend/src/pages/Project/Backends/YAMLForm/index.tsx b/frontend/src/pages/Project/Backends/YAMLForm/index.tsx
index e5ebc10a70..f14e8b1f3a 100644
--- a/frontend/src/pages/Project/Backends/YAMLForm/index.tsx
+++ b/frontend/src/pages/Project/Backends/YAMLForm/index.tsx
@@ -7,11 +7,11 @@ import { Button, FormCodeEditor, FormUI, InfoLink, SpaceBetween } from 'componen
import { useHelpPanel, useNotifications } from 'hooks';
import { isResponseServerError, isResponseServerFormFieldError } from 'libs';
-import { CONFIG_YAML_HELP_ENTERPRISE, CONFIG_YAML_HELP_SKY } from './constants';
+import { CONFIG_YAML_HELP_SELF_HOSTED, CONFIG_YAML_HELP_SKY } from './constants';
import { FieldPath } from 'react-hook-form/dist/types/path';
-const INFO = process.env.UI_VERSION === 'enterprise' ? CONFIG_YAML_HELP_ENTERPRISE : CONFIG_YAML_HELP_SKY;
+const INFO = process.env.UI_VERSION === 'sky' ? CONFIG_YAML_HELP_SKY : CONFIG_YAML_HELP_SELF_HOSTED;
export interface IProps {
initialValues?: IBackendConfigYaml;
diff --git a/frontend/src/pages/Project/Details/index.tsx b/frontend/src/pages/Project/Details/index.tsx
index f667319eb2..74bd1bcf85 100644
--- a/frontend/src/pages/Project/Details/index.tsx
+++ b/frontend/src/pages/Project/Details/index.tsx
@@ -16,22 +16,22 @@ export const ProjectDetails: React.FC = () => {
const matchSettings = useMatch(ROUTES.PROJECT.DETAILS.SETTINGS.FORMAT(paramProjectName));
const matchEvents = useMatch(ROUTES.PROJECT.DETAILS.EVENTS.FORMAT(paramProjectName));
- const tabs: {
- label: string;
- id: string;
- href: string;
- }[] = [
+ const tabs = [
{
label: t('projects.settings'),
id: 'settings',
href: ROUTES.PROJECT.DETAILS.SETTINGS.FORMAT(paramProjectName),
},
- {
+ (process.env.UI_VERSION === 'factory' || process.env.UI_VERSION === 'sky') && {
label: t('projects.events'),
id: 'events',
href: ROUTES.PROJECT.DETAILS.EVENTS.FORMAT(paramProjectName),
},
- ].filter(Boolean);
+ ].filter(Boolean) as {
+ label: string;
+ id: string;
+ href: string;
+ }[];
const showTabs = useMemo(() => {
return Boolean(matchSettings) || Boolean(matchEvents);
diff --git a/frontend/src/pages/Runs/Details/Events/List/index.tsx b/frontend/src/pages/Runs/Details/Events/List/index.tsx
index fb6610033e..178ad717c7 100644
--- a/frontend/src/pages/Runs/Details/Events/List/index.tsx
+++ b/frontend/src/pages/Runs/Details/Events/List/index.tsx
@@ -51,7 +51,13 @@ export const EventsList = () => {
loading={isLoading}
loadingText={t('common.loading')}
header={
- {t('common.full_view')}}>
+ {t('common.full_view')}
+ )
+ }
+ >
{t('navigation.events')}
}
diff --git a/frontend/src/pages/Runs/Details/Jobs/Events/index.tsx b/frontend/src/pages/Runs/Details/Jobs/Events/index.tsx
index 48adc56364..3693f067d5 100644
--- a/frontend/src/pages/Runs/Details/Jobs/Events/index.tsx
+++ b/frontend/src/pages/Runs/Details/Jobs/Events/index.tsx
@@ -64,9 +64,11 @@ export const EventsList = () => {
header={
- {t('common.full_view')}
-
+ (process.env.UI_VERSION === 'factory' || process.env.UI_VERSION === 'sky') && (
+
+ )
}
>
{t('navigation.events')}
diff --git a/frontend/src/pages/User/Details/index.tsx b/frontend/src/pages/User/Details/index.tsx
index 3236d9acad..62cff381e4 100644
--- a/frontend/src/pages/User/Details/index.tsx
+++ b/frontend/src/pages/User/Details/index.tsx
@@ -72,7 +72,7 @@ export const UserDetails: React.FC = () => {
id: UserDetailsTabTypeEnum.PROJECTS,
href: ROUTES.USER.PROJECTS.FORMAT(paramUserName),
},
- {
+ (process.env.UI_VERSION === 'factory' || process.env.UI_VERSION === 'sky') && {
label: t('users.events'),
id: UserDetailsTabTypeEnum.EVENTS,
href: ROUTES.USER.EVENTS.FORMAT(paramUserName),
diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx
index 693d6c7d27..5a94dc6269 100644
--- a/frontend/src/router.tsx
+++ b/frontend/src/router.tsx
@@ -82,7 +82,7 @@ export const router = createBrowserRouter([
index: true,
element: ,
},
- {
+ (process.env.UI_VERSION === 'factory' || process.env.UI_VERSION === 'sky') && {
path: ROUTES.PROJECT.DETAILS.EVENTS.TEMPLATE,
element: ,
},
@@ -102,7 +102,7 @@ export const router = createBrowserRouter([
path: ROUTES.PROJECT.GATEWAY.EDIT.TEMPLATE,
element: ,
},
- ],
+ ].filter(Boolean) as RouteObject[],
},
{
path: ROUTES.PROJECT.DETAILS.RUNS.DETAILS.TEMPLATE,
@@ -191,11 +191,13 @@ export const router = createBrowserRouter([
element: ,
},
- // Events
- {
- path: ROUTES.EVENTS.LIST,
- element: ,
- },
+ // Events, Factory and Sky only
+ ...([
+ (process.env.UI_VERSION === 'factory' || process.env.UI_VERSION === 'sky') && {
+ path: ROUTES.EVENTS.LIST,
+ element: ,
+ },
+ ].filter(Boolean) as RouteObject[]),
// Fleets
{
@@ -302,7 +304,7 @@ export const router = createBrowserRouter([
path: ROUTES.USER.PROJECTS.TEMPLATE,
element: ,
},
- {
+ (process.env.UI_VERSION === 'factory' || process.env.UI_VERSION === 'sky') && {
path: ROUTES.USER.EVENTS.TEMPLATE,
element: ,
},
diff --git a/frontend/src/types/global.d.ts b/frontend/src/types/global.d.ts
index e690164739..5a999b0124 100644
--- a/frontend/src/types/global.d.ts
+++ b/frontend/src/types/global.d.ts
@@ -23,7 +23,7 @@ declare interface HashMap {
declare namespace NodeJS {
interface ProcessEnv {
readonly NODE_ENV: 'development' | 'production' | 'test';
- readonly UI_VERSION: 'sky' | 'enterprise';
+ readonly UI_VERSION: 'sky' | 'factory' | 'oss';
readonly PUBLIC_URL: string;
readonly API_URL: string;
}
diff --git a/frontend/webpack/env.js b/frontend/webpack/env.js
index 6b6e854d4b..02940787b9 100644
--- a/frontend/webpack/env.js
+++ b/frontend/webpack/env.js
@@ -15,9 +15,9 @@ const buildDir = join(__dirname, '../build');
const publicDir = join(__dirname, '../public');
const apiUrl = process.env.API_URL || apiURLs;
const publicUrl = process.env.PUBLIC_URL || publicURLs;
-const uiVersion = process.env.UI_VERSION === 'sky' ? 'sky' : 'enterprise';
+const uiVersion = ['sky', 'factory'].includes(process.env.UI_VERSION) ? process.env.UI_VERSION : 'oss';
-const title = uiVersion === 'enterprise' ? 'dstack' : 'dstack Sky';
+const title = uiVersion === 'sky' ? 'dstack Sky' : 'dstack';
const description =
'Get GPUs at the best prices and availability from a wide range of providers. No cloud ' +
'account of your own is required.\n';
diff --git a/mkdocs/assets/stylesheets/cloudscape-docs.css b/mkdocs/assets/stylesheets/cloudscape-docs.css
index 298a0f0ee7..b8cb27ad8d 100644
--- a/mkdocs/assets/stylesheets/cloudscape-docs.css
+++ b/mkdocs/assets/stylesheets/cloudscape-docs.css
@@ -275,7 +275,7 @@
}
/* "Get started" header dropdown — mirrors the landing's Products popup (featured open-source +
- dstack Sky + Enterprise). Pure-CSS hover/focus-within (the docs are static); right-aligned so the
+ dstack Sky + Factory). Pure-CSS hover/focus-within (the docs are static); right-aligned so the
panel never overflows the header's right edge. Reuses the --cs-* tokens (defined in extra.css). */
.cs-gs-menu {
position: relative;
@@ -303,10 +303,10 @@
z-index: 1000;
width: 360px;
padding: 0;
- background: var(--cs-bg);
- border: 1px solid var(--cs-text);
- border-radius: 12px;
- box-shadow: 0 12px 34px rgba(0, 0, 0, 0.14);
+ /* A floating popup can't show a see-through hole between the groups (arbitrary page content
+ sits behind it) — back the rail with the unselected-row color so the gap reads as ground. */
+ background: var(--cs-panel);
+ border-radius: 8px;
opacity: 0;
visibility: hidden;
transform: translateY(-4px);
@@ -317,88 +317,109 @@
visibility: visible;
transform: translateY(0);
}
-/* Text inside the popup must wrap — Material forces white-space:nowrap on header links/buttons. */
+/* No hole at all in the popup: the groups sit flush, like one continuous menu. */
+.cs-gs-menu__popup .gs-opt + .gs-rail__group {
+ margin-top: 0;
+}
+/* Text inside the popup must wrap — Material forces white-space:nowrap on header links/buttons
+ (the group bars keep their own nowrap). */
.cs-gs-menu__popup,
-.cs-gs-menu__feat,
-.cs-gs-menu__row,
-.cs-gs-menu__feat *,
-.cs-gs-menu__row * {
+.cs-gs-menu__popup .gs-opt,
+.cs-gs-menu__popup .gs-opt * {
white-space: normal !important;
}
-.cs-gs-menu__feat {
- position: relative;
+
+/* The switcher rail — copied pixel-for-pixel from the landing (website/src/styles.css, .gs-rail
+ block): grouped rows under solid bars, with a white indicator card that slides to the hovered
+ row. The docs and the landing are separate builds, so the styles can't be shared — any change
+ to the landing's rail must be mirrored here (and vice versa). */
+.gs-rail {
display: flex;
- align-items: flex-start;
- gap: 12px;
- padding: 11px 10px;
- border-radius: 10px;
- background: var(--cs-text);
- color: var(--cs-bg);
- text-decoration: none;
+ flex-direction: column;
+ padding: 0;
+ position: relative; /* anchors the sliding selection indicator */
+ border-radius: 8px;
+ overflow: hidden; /* clip the full-width group bars to the rounded corners */
+ background: transparent; /* the rows carry the panel ground, so the gap between groups is a true hole */
}
-.cs-gs-menu__feat-body {
- flex: 1 1 auto;
- min-width: 0;
+.gs-rail__indicator {
+ position: absolute;
+ left: 0;
+ right: 0;
+ z-index: 2; /* above the rows' ground, beneath their content (3) and the bars (4) */
+ background: var(--cs-bg);
+ border: 1px solid var(--cs-text);
+ border-radius: 8px;
+ pointer-events: none;
}
-/* Left column: icon tile with the live star count centered beneath it (mirrors the landing). */
-.cs-gs-menu__feat-iccol {
+/* No z-index on the rows: a row with its own stacking context would trap its content beneath
+ the indicator card, so the content is lifted above the card explicitly instead. */
+.gs-opt__icwrap,
+.gs-opt__ic,
+.gs-opt__body {
+ position: relative;
+ z-index: 3;
+}
+.gs-rail a.gs-opt {
+ color: var(--cs-text);
+ text-decoration: none;
+}
+.gs-opt {
+ position: relative;
display: flex;
- flex-direction: column;
- align-items: center;
- gap: 6px;
- flex: 0 0 auto;
+ align-items: flex-start;
+ width: 100%;
+ border-radius: 8px;
+ background: var(--cs-panel); /* each row paints its own ground (the rail is transparent) */
+ font: inherit;
+ line-height: 1.15; /* pinned (not inherited) so the rail matches the landing exactly */
+ text-align: left;
+ cursor: pointer;
}
-.cs-gs-menu__gh {
- color: color-mix(in srgb, var(--cs-bg) 88%, transparent);
- font-size: 12px;
- font-weight: 300;
- font-variant-numeric: tabular-nums;
+/* Adjacent rows inside a group square off their shared edge, so the group's ground reads as one
+ block (rounded corners there would open see-through notches between the rows). */
+.gs-opt + .gs-opt {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
}
-.cs-gs-menu__feat-ic {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- flex: 0 0 auto;
- width: 34px;
- height: 34px;
- border-radius: 9px;
- border: 0.5px solid color-mix(in srgb, var(--cs-bg) 50%, transparent);
- color: var(--cs-bg);
+.gs-opt:has(+ .gs-opt) {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
}
-.cs-gs-menu__feat-ic svg {
- width: 18px;
- height: 18px;
+.gs-opt--feat {
+ padding: 13px 14px;
+ gap: 14px;
}
-.cs-gs-menu__feat-name {
+.gs-opt--row {
+ padding: 13px 14px;
+ gap: 12px;
+}
+.gs-opt__name {
display: block;
- color: var(--cs-bg);
font-size: 16px;
- font-weight: 600;
+ font-weight: 700;
}
-.cs-gs-menu__feat-desc {
+.gs-opt__desc {
display: block;
- margin-top: 8px;
- color: color-mix(in srgb, var(--cs-bg) 88%, transparent);
+ margin-top: 4px;
font-size: 13px;
line-height: 1.5;
+ color: color-mix(in srgb, var(--cs-text) 68%, var(--cs-muted));
}
-.cs-gs-menu__list {
+.gs-opt__icwrap {
display: flex;
flex-direction: column;
+ align-items: center;
+ gap: 6px;
+ flex: 0 0 auto;
}
-.cs-gs-menu__row {
- display: flex;
- align-items: flex-start;
- gap: 12px;
- padding: 11px 10px;
- border-radius: 9px;
- color: var(--cs-text);
- text-decoration: none;
-}
-.cs-gs-menu__row:hover {
- background: var(--cs-hover);
+.gs-opt__stars {
+ font-size: 12px;
+ font-weight: 300;
+ font-variant-numeric: tabular-nums;
+ color: inherit;
}
-.cs-gs-menu__ic {
+.gs-opt__ic {
display: inline-flex;
align-items: center;
justify-content: center;
@@ -406,40 +427,61 @@
width: 34px;
height: 34px;
border-radius: 9px;
- border: 0.5px solid var(--cs-text);
- color: var(--cs-text);
+ border: 1px solid var(--cs-border);
+ color: inherit;
}
-.cs-gs-menu__ic svg {
+.gs-opt__ic svg {
width: 20px;
height: 20px;
}
-.cs-gs-menu__rbody {
+.gs-opt__body {
flex: 1 1 auto;
min-width: 0;
}
-.cs-gs-menu__name {
- display: inline-flex;
- align-items: center;
- gap: 5px;
- color: var(--cs-text);
- font-size: 15px;
- font-weight: 600;
+/* Group bars ("Self-hosted" / "Hosted by us"): thin solid segments that hug the row beneath. */
+.gs-rail__group {
+ position: relative;
+ z-index: 4;
+ background: var(--cs-text);
+ color: var(--cs-bg);
+ font-size: 11px;
+ font-weight: 400;
+ line-height: 1;
+ padding: 5px 16px 5px 60.5px; /* left-aligns the label with the row titles */
+ border-radius: 8px 8px 0 0;
+ white-space: nowrap;
+ margin-bottom: -2px; /* tuck under the next row past any fractional-pixel rounding */
}
-.cs-gs-menu__ext {
- flex: 0 0 auto;
- /* Shared Cloudscape external-link icon (same as footer / GitHub / dstack Sky buttons). */
- width: 0.9em;
- height: 0.9em;
- background-color: currentColor;
- -webkit-mask: var(--cs-ext-icon) center / contain no-repeat;
- mask: var(--cs-ext-icon) center / contain no-repeat;
+/* A group bar that follows a row stands off from it, so the groups read as separate
+ blocks inside the rail (keyed on adjacency, not :first-child — the indicator div
+ is the rail's first DOM child). */
+.gs-opt + .gs-rail__group {
+ margin-top: 10px;
}
-.cs-gs-menu__desc {
- display: block;
- margin-top: 3px;
- color: var(--cs-muted);
- font-size: 13px;
- line-height: 1.5;
+.gs-rail__group:first-child {
+ border-radius: 8px 8px 0 0; /* flush with the borderless rail's 8px clip */
+}
+/* A black top border bridges the bar/row boundary in the middle span; inverted-radius notches
+ fill the space outside the row's top corners so the bar hugs them without hairlines. */
+.gs-rail__group + .gs-opt {
+ border-top: 1px solid var(--cs-text);
+}
+.gs-rail__group + .gs-opt::before,
+.gs-rail__group + .gs-opt::after {
+ content: '';
+ position: absolute;
+ top: -1.5px;
+ width: 9px;
+ height: 9.5px;
+ pointer-events: none;
+}
+.gs-rail__group + .gs-opt::before {
+ left: -1px;
+ background: radial-gradient(circle at 100% 100%, transparent 8px, var(--cs-text) 8.7px);
+}
+.gs-rail__group + .gs-opt::after {
+ right: -1px;
+ background: radial-gradient(circle at 0 100%, transparent 8px, var(--cs-text) 8.7px);
}
/* On tablet/mobile the header buttons collapse; keep the dropdown from overflowing tiny screens. */
@media (max-width: 76.1875em) {
diff --git a/mkdocs/docs/concepts/events.md b/mkdocs/docs/concepts/events.md
index d4057715a2..d70133fcc1 100644
--- a/mkdocs/docs/concepts/events.md
+++ b/mkdocs/docs/concepts/events.md
@@ -22,25 +22,13 @@ Events are accessible through the UI, CLI, and API.
## UI
-The UI allows you to query events either globally on the dedicated `Events` page or within a specific group on the page of a run, job, fleet, and other resources.
-
-### Global page
-
-The global page shows events from all projects that the user has access to and allows filtering by many fields.
-
-{ width=800 }
-
-This page allows you to query events targeting a specific resource or within a particular group.
-
-### Resource page
-
-The resource page shows events within that specific group. For example, if you open a run and switch to the `Events` tab, you will see all events about that run and its jobs.
+In the UI, the `Events` tab is included on the run, job, fleet, and instance pages. It shows events within that specific group. For example, if you open a run and switch to the `Events` tab, you will see all events about that run and its jobs.
{ width=800 }
## CLI
-To query events via the CLI, use the `dstack event` command. This command provides several arguments that allow filtering by target and within scopes.
+To query events via the CLI, use the `dstack event` command.
Here is an example of querying all events within a particular run:
@@ -66,9 +54,17 @@ $ dstack event --within-run cursor
-To see all supported arguments, check the [reference](../reference/cli/dstack/event.md).
+If you invoke the command without arguments, it shows the last events targeting resources in the current project.
+
+The command supports arguments for narrowing down the output:
-If you invoke the command without arguments, it will include all events targeting resources in the project.
+* `--target-fleet`, `--target-run`, `--target-volume`, `--target-gateway`, and `--target-secret` only show events that target the specified resource itself.
+* `--within-fleet`, `--within-run`, and `--within-gateway` also include events about the resources it contains: instances within a fleet, jobs within a run, and replicas within a gateway.
+* `--include-target-type` only shows events that target entities of the specified types, e.g. `job` or `instance`.
+* `--since` only shows events newer than the specified duration (e.g. `10s`, `5m`, `1d`) or RFC 3339 timestamp. If not specified, the last 100 events are shown.
+* `-w` (`--watch`) streams new events in realtime as they are recorded.
+
+To see all supported arguments, check the [reference](../reference/cli/dstack/event.md).
## TTL
diff --git a/mkdocs/docs/concepts/presets.md b/mkdocs/docs/concepts/presets.md
index b48b7d0641..db77a3048f 100644
--- a/mkdocs/docs/concepts/presets.md
+++ b/mkdocs/docs/concepts/presets.md
@@ -242,7 +242,7 @@ Pushing the same name again moves the name to the new preset. The previous one s
Presets are pushed to and pulled from the registry hosted at [dstack Sky](https://sky.dstack.ai). To share a preset, create a project there, add the people you want to share it with, and push the preset to that project. To push or pull a preset from a project, you have to be its member.
-A self-hosted registry is part of [dstack Enterprise](https://calendly.com/dstackai/discovery-call){ target="_blank" }.
+A self-hosted registry is part of [dstack Factory](https://calendly.com/dstackai/discovery-call){ target="_blank" }.
## Export a preset
diff --git a/mkdocs/overrides/header-2.html b/mkdocs/overrides/header-2.html
index f1e6bbd5ff..ab688ef45e 100644
--- a/mkdocs/overrides/header-2.html
+++ b/mkdocs/overrides/header-2.html
@@ -1,6 +1,3 @@
-{#-
- This file was automatically generated - do not edit
--#}
{% set class = "md-header" %}
{% if "navigation.tabs.sticky" in features %}
{% set class = class ~ " md-header--shadow md-header--lifted" %}
@@ -101,8 +98,9 @@
GitHub
- {# "Get started" dropdown — mirrors the landing's Products popup (featured open-source +
- dstack Sky + Enterprise). Pure-CSS hover/focus-within (the site is static), opens below,
+ {# "Get started" dropdown — the landing's product switcher rail as a popup. The product
+ descriptions are duplicated in website/src/components/SiteNavigation.tsx (products) and
+ website/src/pages/Home/GetStartedSection.tsx — keep all three in sync. Opens below,
right-aligned so it never overflows the header's right edge. #}
@@ -176,6 +179,35 @@
});
}
+ // Sliding selection indicator (mirrors the landing rail): rests on the first row, moves
+ // to the hovered row. The popup is hidden via visibility (not display), so offsets are
+ // measurable even while closed.
+ var gsRail = gsMenu && gsMenu.querySelector('.gs-rail');
+ if (gsRail) {
+ var gsInd = gsRail.querySelector('.gs-rail__indicator');
+ var gsOpts = Array.prototype.slice.call(gsRail.querySelectorAll('.gs-opt'));
+ var gsPlace = function () {
+ var on = gsRail.querySelector('.gs-opt--on');
+ if (!on) return;
+ // Fractional rect math (offsetTop/offsetHeight round to integers and leave 1px slivers).
+ var railBox = gsRail.getBoundingClientRect();
+ var rowBox = on.getBoundingClientRect();
+ var borderTop = parseFloat(getComputedStyle(gsRail).borderTopWidth) || 0;
+ gsInd.style.top = (rowBox.top - railBox.top - borderTop) + 'px';
+ gsInd.style.height = rowBox.height + 'px';
+ };
+ gsOpts.forEach(function (opt) {
+ opt.addEventListener('mouseenter', function () {
+ gsOpts.forEach(function (o) { o.classList.remove('gs-opt--on'); });
+ opt.classList.add('gs-opt--on');
+ gsPlace();
+ });
+ });
+ gsPlace();
+ window.addEventListener('resize', gsPlace);
+ gsTrigger.addEventListener('click', gsPlace);
+ }
+
// Live GitHub star count under the "Get started" popup's open-source icon (mirrors the
// landing). Best-effort: if the API is rate-limited or errors, the badge stays hidden.
var el = document.querySelector('[data-cs-gh-stars]');
@@ -189,6 +221,7 @@
else { var k = n / 1000; label = (k >= 10 ? Math.round(k) : Number(k.toFixed(1))) + 'k'; }
el.textContent = label;
el.hidden = false;
+ if (typeof gsPlace === 'function') gsPlace();
})
.catch(function () {});
}
diff --git a/mkdocs/terms.md b/mkdocs/terms.md
index 27188750e3..76d6120689 100644
--- a/mkdocs/terms.md
+++ b/mkdocs/terms.md
@@ -26,7 +26,7 @@ These Terms govern your access to and use of:
These Terms **do not** cover:
- **dstack OSS** — the open-source dstack project is licensed under the Mozilla Public License 2.0. Your use of dstack OSS is governed solely by that license, which is included in the [project repository](https://github.com/dstackai/dstack).
-- **dstack Enterprise** — commercial deployments of dstack Enterprise are governed by a separate written enterprise agreement between you and dstack. These Terms do not apply to dstack Enterprise.
+- **dstack Factory** (formerly dstack Enterprise) — commercial deployments of dstack Factory are governed by a separate written agreement between you and dstack. These Terms do not apply to dstack Factory.
## 3. The Service
@@ -119,7 +119,7 @@ WITHOUT LIMITING THE ABOVE, DSTACK DOES NOT WARRANT THAT:
- ANY PARTICULAR COMPUTE CAPACITY, GPU TYPE, REGION, OR PRICE WILL BE AVAILABLE FROM ANY UPSTREAM CLOUD PROVIDER; OR
- WORKLOADS WILL PRODUCE ANY PARTICULAR RESULT, INCLUDING WITH RESPECT TO MACHINE-LEARNING TRAINING OR INFERENCE OUTPUTS.
-DSTACK DOES NOT OFFER A SERVICE-LEVEL AGREEMENT OR UPTIME COMMITMENT UNDER THESE TERMS. ANY SLA OR SERVICE COMMITMENT MUST BE SEPARATELY AGREED IN WRITING (FOR EXAMPLE, UNDER A DSTACK ENTERPRISE AGREEMENT).
+DSTACK DOES NOT OFFER A SERVICE-LEVEL AGREEMENT OR UPTIME COMMITMENT UNDER THESE TERMS. ANY SLA OR SERVICE COMMITMENT MUST BE SEPARATELY AGREED IN WRITING (FOR EXAMPLE, UNDER A SEPARATE WRITTEN AGREEMENT FOR DSTACK FACTORY).
**Not tailored to industry-specific regulations.** The Service is not designed or tailored to comply with industry-specific laws or regulations such as the Health Insurance Portability and Accountability Act (HIPAA), the Federal Information Security Management Act (FISMA), or the Gramm-Leach-Bliley Act (GLBA). You must not use the Service to process data subject to these or similar regulations unless we have separately agreed in writing to support your specific compliance requirements.
diff --git a/website/src/components/SiteNavigation.tsx b/website/src/components/SiteNavigation.tsx
index 2779644bb5..f9ebef6c49 100644
--- a/website/src/components/SiteNavigation.tsx
+++ b/website/src/components/SiteNavigation.tsx
@@ -1,7 +1,6 @@
-import { ReactNode, useEffect, useRef, useState } from 'react';
+import { Fragment, ReactNode, useEffect, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import Button from '@cloudscape-design/components/button';
-import Icon from '@cloudscape-design/components/icon';
import SideNavigation, { SideNavigationProps } from '@cloudscape-design/components/side-navigation';
import SpaceBetween from '@cloudscape-design/components/space-between';
import { getStartedButtonStyle, menuButtonStyle } from '../cloudscape-theme';
@@ -27,21 +26,14 @@ const GithubGlyph = () => (
);
-const CloudUploadGlyph = () => (
+const CloudGlyph = () => (
);
-const FingerprintGlyph = () => (
+const BoxGlyph = () => (
);
@@ -61,15 +53,18 @@ type ProductLink = {
href: string;
external?: boolean;
icon: ReactNode;
+ badge: string;
};
// The products. products[0] (open-source) is featured at the top of the "Products" menu; the rest
// follow as rows. Reused by the standalone top-nav hover menu and the mobile nav's "Products"
// section.
+// NOTE: the descriptions are duplicated in GetStartedSection.tsx (the switcher rail) and
+// mkdocs/overrides/header-2.html — keep all three in sync.
const products: ProductLink[] = [
- { id: 'open-source', text: 'dstack', secondaryText: 'The open-source control plane that works across clouds, Kubernetes, and on-prem.', href: DOCS_URL, icon: },
- { id: 'enterprise', text: 'Enterprise', secondaryText: 'Self-hosted with SSO, air-gapped setup, dedicated support, and more.', href: 'https://calendly.com/dstackai/discovery-call', external: true, icon: },
- { id: 'sky-product', text: 'dstack Sky', secondaryText: 'Access GPU marketplace, or bring your own clouds. Hosted and managed by us.', href: 'https://sky.dstack.ai', external: true, icon: },
+ { id: 'open-source', text: 'dstack', secondaryText: 'The open-source control plane for AI-native orchestration.', href: DOCS_URL, icon: , badge: 'Self-hosted' },
+ { id: 'factory', text: 'dstack Factory', secondaryText: 'Extends dstack with a governance and metering layer, and a private preset registry.', href: 'https://calendly.com/dstackai/discovery-call', external: true, icon: , badge: 'Self-hosted' },
+ { id: 'sky-product', text: 'dstack Sky', secondaryText: 'Everything in dstack Factory, plus the GPU marketplace and the public preset registry.', href: 'https://sky.dstack.ai', external: true, icon: , badge: 'Hosted by us' },
];
// Items for the mobile slide-out navigation. The blog categories are top-level links (mirroring
@@ -126,6 +121,23 @@ function ProductsHoverMenu() {
.catch(() => {});
}, [open]);
+ // The popup renders the Get-started section's switcher rail verbatim. The white indicator card
+ // rests on the first row and slides to whichever row the pointer is over.
+ const [active, setActive] = useState(0);
+ const railRef = useRef(null);
+ const [indicator, setIndicator] = useState<{ top: number; height: number } | null>(null);
+ useEffect(() => {
+ if (!open) return;
+ const rail = railRef.current;
+ const selected = rail?.querySelector('.gs-opt--on');
+ if (!rail || !selected) return;
+ const railBox = rail.getBoundingClientRect();
+ const rowBox = selected.getBoundingClientRect();
+ // Fractional rect math (offsetTop/offsetHeight round to integers and leave 1px slivers).
+ const borderTop = parseFloat(getComputedStyle(rail).borderTopWidth) || 0;
+ setIndicator({ top: rowBox.top - railBox.top - borderTop, height: rowBox.height });
+ }, [open, active, stars]);
+
return (
@@ -271,14 +290,21 @@ export function SiteNavigation({
>
GitHub
-
+
+
+
diff --git a/website/src/pages/Home/ExploreSection.tsx b/website/src/pages/Home/ExploreSection.tsx
index d4de38682a..5b8794d6e7 100644
--- a/website/src/pages/Home/ExploreSection.tsx
+++ b/website/src/pages/Home/ExploreSection.tsx
@@ -1,58 +1,22 @@
-import CodeView from '@cloudscape-design/code-view/code-view';
-import yamlHighlight from '@cloudscape-design/code-view/highlight/yaml';
-import Button from '@cloudscape-design/components/button';
+import { useState } from 'react';
import Icon from '@cloudscape-design/components/icon';
-import Tabs from '@cloudscape-design/components/tabs';
-import { mainButtonStyle } from '../../cloudscape-theme';
import { AlternatingDocBlock } from '../../components/AlternatingDocBlock';
import { ArchitectureDiagram } from '../../components/ArchitectureDiagram';
import { DashedBorder } from '../../components/DashedBorder';
import { highlightTerms } from '../../components/highlightTerms';
-import { gpuOffers } from '../../data/gpus';
import { docsUrl } from '../../routes';
-import {
- backendConfigs,
- clusterConfigs,
- maxBackendYamlLines,
- maxClusterYamlLines,
- padYamlToLines,
-} from '../../data/snippets';
+import { CapList, CloudGlyph, KubernetesGlyph, ServerGlyph } from './GetStartedSection';
-// Core orchestration primitives shown in the "AI-native orchestration" block.
+// Core orchestration primitives shown in the "AI-native orchestration" block. "Runs" folds the
+// three run types (dev environments, tasks, services) into one card; there's no single runs
+// concept page, so it links to the quickstart ("creating fleets and submitting runs").
const keyConcepts = [
{ name: 'Fleets', label: 'Cloud & on-prem', href: docsUrl('concepts/fleets'), description: 'Provision and manage clusters across clouds, Kubernetes, and on-prem.' },
- { name: 'Dev environments', label: 'Development', href: docsUrl('concepts/dev-environments'), description: 'Launch dev environments to be accessed by agents or from your IDE.' },
- { name: 'Tasks', label: 'Training and batch', href: docsUrl('concepts/tasks'), description: 'Run training and batch jobs across a single node or clusters.' },
- { name: 'Services', label: 'Model inference', href: docsUrl('concepts/services'), description: 'Deploy model inference as secure and scalable endpoints.' },
+ { name: 'Runs', label: 'Dev, training, and inference', href: docsUrl('quickstart'), description: 'Run dev environments, training tasks, and inference services on your fleets.' },
+ { name: 'Gateways', label: 'Ingress', href: docsUrl('concepts/gateways'), description: 'Manage auto-scaling, rate limits, ingress, custom domains, etc.' },
+ { name: 'Presets', label: 'Inference optimization', href: docsUrl('concepts/presets'), description: 'Agent-based inference optimization toolkit, and a preset registry.' },
];
-// Read-only YAML snippet. Line wrapping is left off so one line maps to one row,
-// which keeps padded snippets equal height across tabs (see padYamlToLines).
-function YamlCode({ content }: { content: string }) {
- return (
-
-
-
- );
-}
-
-// GPU price list — a plain monospace name/price list in a bordered card, matching the dstack Sky
-// "GPU marketplace" pane in the Get started section (same .gs-mkt__row treatment, single source).
-function GpuMarketplaceTable() {
- return (
-
- );
-}
-
// The main marketing content: a sequence of alternating documentation blocks.
export function ExploreSection() {
return (
@@ -66,55 +30,99 @@ export function ExploreSection() {
- ({
- id: backend.id,
- label: backend.label,
- content: ,
- }))}
- />
- }
- title="Bring your own clouds"
- imageFirst
- >
- dstack natively integrates with the major GPU clouds and automates provisioning of clusters.
-
-
- Authorize dstack by providing credentials, and dstack will provision compute and schedule workloads
- in your own cloud account.
-
+
- ({
- id: cluster.id,
- label: cluster.label,
- content: ,
- }))}
- />
- }
- title="Bring on-prem clusters"
- >
- Have an existing Kubernetes cluster? Point dstack to the kubeconfig, and dstack
- will schedule workloads on it as it was a cloud cluster.
-
-
- Have bare-metal servers or VMs with SSH access? Point dstack to those hosts and provide SSH credentials, and dstack will
- schedule workloads on them alongside Kubernetes and cloud clusters.
-
-
-
);
}
+// Clouds grid for the merged compute block, grouped per column: traditional hyperscalers,
+// top GPU neoclouds, then smaller GPU clouds.
+const CLOUD_GROUPS = [
+ ['AWS', 'GCP', 'Azure', 'OCI', 'DigitalOcean', 'Vultr'],
+ ['Nebius', 'Crusoe', 'Lambda', 'Verda', 'Runpod'],
+ ['AMD Dev Cloud', 'Hot Aisle', 'Vast.ai', 'JarvisLabs'],
+];
+
+// On-prem capability rows for the merged compute block (same shape as the Factory CapList tabs).
+const onPremItems = [
+ { icon: , title: 'SSH fleets', sub: 'Attach bare-metal servers or VMs with SSH access' },
+ { icon: , title: 'Kubernetes', sub: 'Attach your existing Kubernetes clusters' },
+];
+
+// One block for both compute targets: the Get-started panes' tabbed box as the visual (on-prem
+// rows / the clouds grid), with a footer note that swaps with the selected tab (like the dstack
+// Sky pane's notes). The prose beside it is static and condenses the former per-target blocks.
+// These are core dstack capabilities, so they live here rather than under dstack Factory.
+function BringComputeBlock() {
+ const [pane, setPane] = useState<'onprem' | 'clouds'>('onprem');
+ return (
+
+
+ {pane === 'onprem' && (
+ Bring bare-metal servers, a Kubernetes cluster, or just VMs
+ )}
+ {pane === 'clouds' && (
+ Configure credentials for your clouds to automate provisioning
+ )}
+
+
+ }
+ title="Bring your own compute"
+ imageFirst
+ >
+ dstack natively integrates with the major GPU clouds and automates provisioning of clusters.
+ Authorize dstack by configuring backends with your credentials, and dstack will provision fleets
+ and schedule workloads in your own cloud account.
+
+
+ Have bare-metal servers or VMs with SSH access? Point dstack to those hosts and provide SSH
+ credentials to create an SSH fleet. Have an existing Kubernetes cluster? Point dstack's
+ Kubernetes backend to the kubeconfig. dstack will schedule workloads on them alongside cloud
+ clusters.
+
+ );
+}
+
function KeyConceptsBlock() {
return (
);
}
-
-function GpuMarketplaceBlock() {
- return (
- }
- title="Access marketplace GPUs"
- imageFirst
- action={}
- >
- Don't have your own cloud accounts or on-prem clusters? No problem. You can access compute
- through dstack Sky, our hosted GPU marketplace.
-
-
- It's possible to use dstack Sky alongside with your own cloud accounts or on-prem clusters.
-
- );
-}
diff --git a/website/src/pages/Home/FaqSection.tsx b/website/src/pages/Home/FaqSection.tsx
index 4a9e81d430..4da0f24835 100644
--- a/website/src/pages/Home/FaqSection.tsx
+++ b/website/src/pages/Home/FaqSection.tsx
@@ -31,6 +31,8 @@ export function FaqSection() {
return (
+ {/* No imageFirst: the merged compute block above is visual-left, so the FAQ alternates back
+ to visual-right. */}
diff --git a/website/src/pages/Home/GetStartedSection.tsx b/website/src/pages/Home/GetStartedSection.tsx
index 6324ab4e8b..18f51c825f 100644
--- a/website/src/pages/Home/GetStartedSection.tsx
+++ b/website/src/pages/Home/GetStartedSection.tsx
@@ -1,11 +1,11 @@
-import { KeyboardEvent, useEffect, useState } from 'react';
+import { KeyboardEvent, ReactNode, useEffect, useRef, useState } from 'react';
import CodeView from '@cloudscape-design/code-view/code-view';
import shHighlight from '@cloudscape-design/code-view/highlight/sh';
import Button from '@cloudscape-design/components/button';
import { mainButtonStyle } from '../../cloudscape-theme';
import { gpuOffers } from '../../data/gpus';
import { installMethods, maxInstallLines, padYamlToLines } from '../../data/snippets';
-import { docsUrl } from '../../routes';
+import { DOCS_URL, docsUrl } from '../../routes';
const GITHUB_API_URL = 'https://api.github.com/repos/dstackai/dstack';
@@ -16,37 +16,25 @@ function formatStars(count: number): string {
return `${thousands >= 10 ? Math.round(thousands) : Number(thousands.toFixed(1))}k`;
}
-// Product glyphs. GitHub mark doubles as the open-source star badge; cloud / fingerprint mark the
+// Product glyphs. GitHub mark doubles as the open-source star badge; cloud / box mark the
// hosted / self-hosted rows (thin-line, matching the Products menu).
const GithubGlyph = () => (
);
-const CloudUploadGlyph = () => (
+export const CloudGlyph = () => (
);
-const FingerprintGlyph = () => (
+const BoxGlyph = () => (
);
-const CheckGlyph = () => (
-
-);
-// Enterprise capability glyphs (thin-line, matching the Products menu). Distinct from the
-// fingerprint product mark in the switcher — one icon per capability.
+// Factory capability glyphs (thin-line, matching the Products menu). Distinct from the
+// box product mark in the switcher — one icon per capability.
const KeyGlyph = () => (
);
-const SupportGlyph = () => (
-
-);
const AuditGlyph = () => (
);
+// GPU chip glyph for the marketplace rows.
+const ChipGlyph = () => (
+
+);
+// On-prem glyphs (shared with the landing's "bring your own compute" block).
+export const ServerGlyph = () => (
+
+);
+// The Kubernetes helm wheel from /static/logos/kubernetes.svg, inlined with currentColor so it
+// takes the capability-icon color in both themes (the asset has hardcoded black fills).
+export const KubernetesGlyph = () => (
+
+);
// Read-only shell snippet. Line wrapping is left off so padded snippets stay
// equal height across tabs (see padYamlToLines).
@@ -84,28 +104,94 @@ function ShellCode({ content }: { content: string }) {
);
}
-type DeployTab = 'oss' | 'sky' | 'ent';
+type DeployTab = 'oss' | 'sky' | 'factory';
-// dstack Sky: a sample of the GPU marketplace (price ranges, no provider/region — the point is
-// "on-demand GPUs at a price"; same offers as the "Access marketplace GPUs" block) and the clouds
-// you can bring instead. Both lists scroll.
-const SKY_CLOUDS = ['AWS', 'GCP', 'Azure', 'Kubernetes', 'Lambda', 'RunPod', 'Nebius', 'OCI', 'Vast.ai', 'CloudRift', 'Crusoe', 'SSH fleets'];
-// Enterprise "Extra" capabilities (beyond open-source). One tab for now; more can be added later.
-const ENT_CAPS = [
- { icon: , title: 'Single Sign-On (SSO)', sub: 'Okta, Microsoft Entra, Google Workspace' },
- { icon: , title: 'Air-gapped deployment', sub: 'Run fully offline, in your own VPC' },
- { icon: , title: 'Dedicated support', sub: 'Bug fixes, feature prioritization, SLAs' },
- { icon: , title: 'Audit logs & RBAC', sub: 'Fine-grained roles and audit trails' },
+const UserGlyph = () => (
+
+);
+const ShareGlyph = () => (
+
+);
+// Remaining Factory glyphs, same thin-line style as the capability icons.
+const GaugeGlyph = () => (
+
+);
+const LayersGlyph = () => (
+
+);
+
+// dstack Factory tab contents: icon + title + subtitle rows per tab. Only Factory-exclusive
+// capabilities live here — bringing clouds/on-prem is core dstack and is shown in the Explore
+// section's "bring your own compute" block instead.
+// Rendered with column flow (see .gs-caps--cols): the first three fill the left column, the
+// rest the right — and the mobile single column keeps this same order.
+const FACTORY_GOVERNANCE = [
+ { icon: , title: 'Single Sign-On (SSO)', sub: 'Okta, Microsoft Entra, Google Workspace' },
+ { icon: , title: 'Audit logs', sub: 'Track system events across tenants (projects and users)' },
+ { icon: , title: 'Fine-grained tokens', sub: 'Multiple user tokens with fine-grained permissions' },
+ { icon: , title: 'Sharable resources', sub: 'Share fleets and gateways across tenants (projects)' },
+ { icon: , title: 'Compute metering', sub: 'Usage metering and quotas per tenant (project or user)' },
+ { icon: , title: 'Air-gapped deployment', sub: 'Runs in isolated environments with no Internet access' },
+];
+// The preset registry: private (your own) in dstack Factory, public in dstack Sky.
+const FACTORY_PRESETS = [
+ { icon: , title: 'Private registry', sub: 'Sharing optimized inference serving configurations' },
+ { icon: , title: 'Token metering', sub: 'Token metering and quotas per tenant (project or user)' },
+];
+const SKY_PRESETS = [
+ { icon: , title: 'Public registry', sub: 'Sharing optimized inference serving configurations' },
];
-// Closing "Get started" section. The Products-popup component is reused as a vertical switcher
-// (open-source featured + selected by default; dstack Sky / Enterprise as rows). Each tab's detail
-// is a bordered box with a footer-bar CTA: open-source shows the install code, Sky shows the GPU
-// marketplace alongside the clouds you can bring, Enterprise is a placeholder for now.
+// Icon + title + subtitle rows, shared by the Factory tabs and the explore block.
+export function CapList({ items, columnFlow }: { items: { icon: ReactNode; title: string; sub: string }[]; columnFlow?: boolean }) {
+ return (
+
+ {items.map(cap => (
+
+ {cap.icon}
+
+ {cap.title}
+ {cap.sub}
+
+
+ ))}
+
+ );
+}
+
+// Closing "Get started" section: the product switcher rail (also rendered by the Products menu
+// and the docs header popup) beside each product's detail box. Open-source shows the install
+// code, Factory shows governance and its private preset registry, Sky the GPU marketplace and
+// the public preset registry.
+// NOTE: the three product descriptions below are duplicated in SiteNavigation.tsx (products)
+// and mkdocs/overrides/header-2.html — keep all three in sync.
export function GetStartedSection() {
const [tab, setTab] = useState('oss');
+ // The selection indicator: one white card that slides to the selected row.
+ const railRef = useRef(null);
+ const [indicator, setIndicator] = useState<{ top: number; height: number } | null>(null);
const [method, setMethod] = useState<(typeof installMethods)[number]['id']>(installMethods[0].id);
+ // dstack Sky and Factory panes: which of their tabs is shown (same interaction as the
+ // install-method tabs).
+ const [skyPane, setSkyPane] = useState<'marketplace' | 'presets'>('marketplace');
+ const [factoryPane, setFactoryPane] = useState<'governance' | 'registry'>('governance');
const [stars, setStars] = useState(null);
// Live star count for the open-source tile, fetched once. Best-effort: if the API is rate-limited
@@ -140,13 +226,33 @@ export function GetStartedSection() {
const activeInstall = installMethods.find(m => m.id === method) ?? installMethods[0];
+ // Keep the indicator glued to the selected row (on selection change, resize, and once the star
+ // count loads — it grows the first row). Fractional rect math, not offsetTop/offsetHeight: those
+ // round to integers while the layout is fractional, leaving up-to-1px slivers at the row edges.
+ useEffect(() => {
+ const update = () => {
+ const rail = railRef.current;
+ const selected = rail?.querySelector('.gs-opt--on');
+ if (!rail || !selected) return;
+ const railBox = rail.getBoundingClientRect();
+ const rowBox = selected.getBoundingClientRect();
+ const borderTop = parseFloat(getComputedStyle(rail).borderTopWidth) || 0;
+ setIndicator({ top: rowBox.top - railBox.top - borderTop, height: rowBox.height });
+ };
+ update();
+ window.addEventListener('resize', update);
+ return () => window.removeEventListener('resize', update);
+ }, [tab, stars]);
+
return (
Get started
{/* Left: the popup-style selector. */}
-
+
+ {indicator && }
+
Self-hosted
@@ -156,23 +262,24 @@ export function GetStartedSection() {
dstack
- The open-source control plane that works across clouds, Kubernetes, and on-prem.
+ The open-source control plane for AI-native orchestration.
-
-
+
+
- Enterprise
- Self-hosted with SSO, air-gapped setup, dedicated support, and more.
+ dstack Factory
+ Extends dstack with a governance and metering layer, and a private preset registry.
+
Hosted by us
-
+ dstack Sky
- Access GPU marketplace, or bring your own clouds. Hosted and managed by us.
+ Everything in dstack Factory, plus the GPU marketplace and the public preset registry.
@@ -199,81 +306,105 @@ export function GetStartedSection() {
-
- Use with your own clouds, Kubernetes, and on-prem clusters
- Use with your own clouds & clusters
-
+
- {/* Two panes with tab-styled headers. Source order is header→list, header→list, so on
- mobile (single column) each header sits directly above its own list; on desktop a
- 2-row grid (auto-flow column) puts both headers in a row and both lists beneath. */}
-
-
GPU marketplace
-
-
+ {/* Real tabs, like the open-source box's uv/pip/Docker — one pane shown at a time. */}
+
-
- Sign up to get $5 credit for on-demand and spot instances
- Sign up to get $5 in credits
-
+ {skyPane === 'marketplace' && (
+
+ Access compute from a pool of providers. Sign up to get $5 credit.
+ Sign up to get $5 credit
+
+ )}
+ {skyPane === 'presets' && (
+ Pull ready-to-deploy presets from the public registry
+ )}
+
)}
- {/* Enterprise: the open-source box's tabbed shell, with a single "Extra" tab for now. */}
- {tab === 'ent' && (
-
+ {/* dstack Factory: only the Factory-exclusive capabilities — governance first, then the
+ private preset registry. */}
+ {tab === 'factory' && (
+
-
- Talk to our team to get answers and a free trial
- Talk to our team for a free trial
-
-
+ {factoryPane === 'registry' && (
+ Push and pull presets within your organization
+ )}
+
diff --git a/website/src/pages/Home/HomePage.tsx b/website/src/pages/Home/HomePage.tsx
index 96c4140016..2b9a8788a8 100644
--- a/website/src/pages/Home/HomePage.tsx
+++ b/website/src/pages/Home/HomePage.tsx
@@ -33,17 +33,24 @@ export function HomePage() {
)}