Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
23 changes: 22 additions & 1 deletion frontend/src/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"tutorial_other": "Take a tour",
"docs": "Docs",
"discord": "Discord",
"danger_zone": "Danger Zone",
"danger_zone": "Danger zone",
"control_plane": "Control plane",
"refresh": "Refresh",
"quickstart": "Quickstart",
Expand Down Expand Up @@ -223,10 +223,26 @@
"members_empty_message_text": "Select project's members",
"update_members_success": "Members are updated",
"update_visibility_success": "Project visibility updated successfully",
"update_templates_repo_success": "Templates updated successfully",
"update_visibility_confirm_title": "Change project visibility",
"update_visibility_confirm_message": "Are you sure you want to change the project visibility? This will affect who can access this project.",
"change_visibility": "Change",
"project_visibility": "Visibility",
"project_visibility_settings": "Change project visibility",
"templates_repo": "Templates",
"override_project_templates": "Configure project templates",
"transfer_ownership": "Transfer ownership",
"templates_repo_description": "Set a project-level templates repository URL",
"templates_repo_placeholder": "https://github.com/org/templates.git",
"templates_repo_not_set": "not set",
"templates_repo_required": "Templates repo URL cannot be empty",
"save_templates_repo": "Save",
"configure_templates_repo": "Configure",
"change_templates_repo_title": "Override project templates",
"change_templates_repo_message": "Specify a new templates Git repo URL:",
"reset_templates_repo": "Reset",
"reset_templates_repo_title": "Reset templates",
"reset_templates_repo_message": "Are you sure you want to reset templates for this project?",
"project_visibility_description": "Control who can access this project",
"make_project_public": "Make project public",
"delete_project_confirm_title": "Delete project",
Expand Down Expand Up @@ -472,6 +488,11 @@
},
"runs": {
"launch_button": "Launch",
"no_templates_alert": {
"title": "No templates configured",
"description": "The selected project has no templates available for Launch.",
"action": "Settings"
},
"launch": {
"wizard": {
"title": "Launch",
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/pages/Project/Details/Settings/constants.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import Link from '@cloudscape-design/components/link';

export const CLI_INFO = {
header: <h2>CLI</h2>,
Expand All @@ -21,3 +22,23 @@ export const CLI_INFO = {
</>
),
};

export const TEMPLATES_REPO_INFO = {
header: <h2>Templates</h2>,
body: (
<>
<p>
Specify a project-level templates Git repository URL. Templates from this repo are shown on the Launch page in
Runs, and setting it enables the Launch button when templates are available.
</p>
<p>If set, project templates override global templates configured on the server.</p>
<p>
See official examples in{' '}
<Link href="https://github.com/dstackai/dstack-templates" external>
dstackai/dstack-templates
</Link>
.
</p>
</>
),
};
391 changes: 306 additions & 85 deletions frontend/src/pages/Project/Details/Settings/index.tsx

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions frontend/src/pages/Project/Details/Settings/styles.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,27 @@
width: 300px;
}

.templatesRepoRow {
display: flex;
align-items: center;
gap: 12px;
}

.templatesRepoTitle {
display: inline-flex;
align-items: center;
gap: 8px;
}

.templatesRepoInput {
width: 300px;
max-width: 100%;
}

.templatesRepoActions {
flex-shrink: 0;
}

.codeWrapper {
position: relative;

Expand Down
29 changes: 21 additions & 8 deletions frontend/src/pages/Runs/Launch/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { WizardProps } from '@cloudscape-design/components';
import { CardsProps } from '@cloudscape-design/components/cards';

import {
Button,
Container,
FormCards,
FormCodeEditor,
Expand Down Expand Up @@ -234,6 +235,10 @@ export const Launch: React.FC = () => {
onSubmitWizard().catch(console.log);
}
};
const openProjectSettings = () => {
if (!formValues.project) return;
navigate(`${ROUTES.PROJECT.DETAILS.SETTINGS.FORMAT(formValues.project)}#danger-zone`);
};

const envParam = selectedTemplate?.parameters?.find((p) => p.type === 'env');
const yaml = useGenerateYaml({
Expand Down Expand Up @@ -320,6 +325,20 @@ export const Launch: React.FC = () => {
},
],
}}
empty={
formValues.project ? (
<SpaceBetween size="xs" direction="vertical">
<div>{t('runs.no_templates_alert.description')}</div>
<div>
<Button formAction="none" onClick={openProjectSettings}>
{t('runs.no_templates_alert.action')}
</Button>
</div>
</SpaceBetween>
) : (
t('runs.launch.wizard.template_placeholder')
)
}
cardsPerRow={[{ cards: 1 }, { minWidth: 400, cards: 2 }, { minWidth: 800, cards: 3 }]}
onSelectionChange={onChangeTemplate}
/>
Expand All @@ -346,11 +365,7 @@ export const Launch: React.FC = () => {
defaultValue={false}
toggleLabel={t('runs.launch.wizard.gpu')}
toggleDescription={t('runs.launch.wizard.gpu_description')}
errorText={
formValues.gpu_enabled
? formState.errors.offer?.message
: undefined
}
errorText={formValues.gpu_enabled ? formState.errors.offer?.message : undefined}
name={FORM_FIELD_NAMES.gpu_enabled}
/>
}
Expand All @@ -371,9 +386,7 @@ export const Launch: React.FC = () => {
control={control}
label={t('runs.launch.wizard.configuration_label')}
description={t('runs.launch.wizard.configuration_description')}
info={
<InfoLink onFollow={() => openHelpPanel(CONFIGURATION_INFO)} />
}
info={<InfoLink onFollow={() => openHelpPanel(CONFIGURATION_INFO)} />}
name={FORM_FIELD_NAMES.config_yaml}
language="yaml"
loading={loading}
Expand Down
3 changes: 0 additions & 3 deletions frontend/src/pages/Runs/List/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ export const RunList: React.FC = () => {
filteringStatusType,
handleLoadItems,
} = useFilters();

const projectHavingFleetMap = useCheckingForFleetsInProjects({});

const { data, isLoading, refreshList, isLoadingMore } = useInfiniteScroll<IRun, TRunsRequestParams>({
Expand Down Expand Up @@ -120,7 +119,6 @@ export const RunList: React.FC = () => {
}`,
);
};

const projectDontHasFleet = Object.keys(projectHavingFleetMap).find((project) => !projectHavingFleetMap[project]);

return (
Expand All @@ -143,7 +141,6 @@ export const RunList: React.FC = () => {
show={!!projectDontHasFleet}
dismissible={true}
/>

<Header
variant="awsui-h1-sticky"
actions={
Expand Down
14 changes: 11 additions & 3 deletions frontend/src/services/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,11 +194,19 @@ export const projectApi = createApi({
providesTags: () => ['ProjectRepos'],
}),

updateProject: builder.mutation<IProject, { project_name: string; is_public: boolean }>({
query: ({ project_name, is_public }) => ({
updateProject: builder.mutation<
IProject,
{
project_name: string;
is_public?: boolean;
templates_repo?: string | null;
reset_templates_repo?: boolean;
}
>({
query: ({ project_name, ...body }) => ({
url: API.PROJECTS.UPDATE(project_name),
method: 'POST',
body: { is_public },
body,
}),
transformResponse: transformProjectResponse,
invalidatesTags: (result, error, params) => [{ type: 'Projects' as const, id: params?.project_name }],
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/types/project.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ declare interface IProject {
owner: IUser | { username: string };
created_at: string;
isPublic: boolean;
templates_repo?: string | null;
}

declare interface IProjectMember {
Expand All @@ -55,4 +56,5 @@ declare interface IProjectSecret {

declare type IProjectCreateRequestParams = Pick<IProject, 'project_name'> & {
is_public: boolean;
templates_repo?: string | null;
};
1 change: 1 addition & 0 deletions src/dstack/_internal/core/models/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class Project(CoreModel):
backends: List[BackendInfo]
members: List[Member]
is_public: bool = False
templates_repo: Optional[str] = None


class ProjectsInfoList(CoreModel):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Add ProjectModel.templates_repo

Revision ID: a13f5b55af01
Revises: 5e8c7a9202bc
Create Date: 2026-03-06 12:00:00.000000

"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "a13f5b55af01"
down_revision = "c7b0a8e57294"
branch_labels = None
depends_on = None


def upgrade() -> None:
with op.batch_alter_table("projects", schema=None) as batch_op:
batch_op.add_column(sa.Column("templates_repo", sa.Text(), nullable=True))


def downgrade() -> None:
with op.batch_alter_table("projects", schema=None) as batch_op:
batch_op.drop_column("templates_repo")
1 change: 1 addition & 0 deletions src/dstack/_internal/server/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ class ProjectModel(BaseModel):
name: Mapped[str] = mapped_column(String(50), unique=True)
created_at: Mapped[datetime] = mapped_column(NaiveDateTime, default=get_current_datetime)
is_public: Mapped[bool] = mapped_column(Boolean, default=False)
templates_repo: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
deleted: Mapped[bool] = mapped_column(Boolean, default=False)
original_name: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
"""`original_name` stores the deleted project's original name while `name` is changed to a unique
Expand Down
3 changes: 3 additions & 0 deletions src/dstack/_internal/server/routers/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ async def create_project(
user=user,
project_name=body.project_name,
is_public=body.is_public,
templates_repo=body.templates_repo,
)
)

Expand Down Expand Up @@ -200,6 +201,8 @@ async def update_project(
user=user,
project=project,
is_public=body.is_public,
templates_repo=body.templates_repo,
reset_templates_repo=body.reset_templates_repo,
)
await session.refresh(project)
return CustomORJSONResponse(projects.project_model_to_project(project))
3 changes: 2 additions & 1 deletion src/dstack/_internal/server/routers/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@
async def list_templates(
user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
):
return CustomORJSONResponse(await templates_service.list_templates())
_, project = user_project
return CustomORJSONResponse(await templates_service.list_templates(project))
5 changes: 4 additions & 1 deletion src/dstack/_internal/server/schemas/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,13 @@ class ListProjectsRequest(CoreModel):
class CreateProjectRequest(CoreModel):
project_name: str
is_public: bool = False
templates_repo: Optional[str] = None


class UpdateProjectRequest(CoreModel):
is_public: bool
is_public: Optional[bool] = None
templates_repo: Optional[str] = None
reset_templates_repo: bool = False


class DeleteProjectsRequest(CoreModel):
Expand Down
Loading