diff --git a/.github/workflows/web-test-unit-tests.yml b/.github/workflows/web-test-unit-tests.yml
index 68c539bee3c7..ceb4c8307c36 100644
--- a/.github/workflows/web-test-unit-tests.yml
+++ b/.github/workflows/web-test-unit-tests.yml
@@ -2,7 +2,10 @@ name: Unit tests for web code
on:
pull_request:
paths:
- - ".github/workflows/web-test.yml"
+ - ".github/workflows/web-test-unit-tests.yml"
+ - ".nvmrc"
+ - "package.json"
+ - "package-lock.json"
- "web-admin/**"
- "web-common/**"
- "web-local/**"
@@ -19,10 +22,17 @@ jobs:
with:
filters: |
admin:
- - ".github/workflows/web-test.yml"
+ - ".github/workflows/web-test-unit-tests.yml"
+ - ".nvmrc"
+ - "package.json"
+ - "package-lock.json"
- "web-admin/**"
+ - "web-common/**"
common:
- - ".github/workflows/web-test.yml"
+ - ".github/workflows/web-test-unit-tests.yml"
+ - ".nvmrc"
+ - "package.json"
+ - "package-lock.json"
- "web-common/**"
- name: Set up NodeJS
@@ -40,3 +50,7 @@ jobs:
- name: Run web-common unit tests
if: steps.filter.outputs.common == 'true'
run: npm run test -w web-common
+
+ - name: Run web-admin unit tests
+ if: steps.filter.outputs.admin == 'true'
+ run: npm run test:unit -w web-admin
diff --git a/admin/deployments.go b/admin/deployments.go
index 668e0a191b0a..b7ba128797c4 100644
--- a/admin/deployments.go
+++ b/admin/deployments.go
@@ -136,9 +136,10 @@ func (s *Service) TeardownDeployment(ctx context.Context, depl *database.Deploym
}
// UpdateDeploymentsForProject updates the deployments of a project.
+// An empty environment updates deployments in all environments.
// Care must be taken to avoid one broken deployment from blocking updates to other healthy deployments.
-func (s *Service) UpdateDeploymentsForProject(ctx context.Context, p *database.Project) error {
- ds, err := s.DB.FindDeploymentsForProject(ctx, p.ID, "", "")
+func (s *Service) UpdateDeploymentsForProject(ctx context.Context, p *database.Project, environment string) error {
+ ds, err := s.DB.FindDeploymentsForProject(ctx, p.ID, environment, "")
if err != nil {
return err
}
diff --git a/admin/project_slots_test.go b/admin/project_slots_test.go
new file mode 100644
index 000000000000..99fd4ae9f594
--- /dev/null
+++ b/admin/project_slots_test.go
@@ -0,0 +1,111 @@
+package admin
+
+import (
+ "context"
+ "sync"
+ "testing"
+
+ "github.com/rilldata/rill/admin/database"
+ "github.com/rilldata/rill/admin/jobs"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/zap"
+)
+
+func TestUpdateProjectSlots(t *testing.T) {
+ tests := []struct {
+ name string
+ prodSlots int
+ devSlots int
+ subpath string
+ expectedIDs []string
+ }{
+ {name: "unchanged", prodSlots: 2, devSlots: 2},
+ {name: "production only", prodSlots: 4, devSlots: 2, expectedIDs: []string{"prod-deployment", "prod-branch-deployment"}},
+ {name: "development only", prodSlots: 2, devSlots: 4, expectedIDs: []string{"dev-deployment", "dev-branch-deployment"}},
+ {name: "both environments", prodSlots: 4, devSlots: 4, expectedIDs: []string{"prod-deployment", "prod-branch-deployment", "dev-deployment", "dev-branch-deployment"}},
+ {name: "shared configuration", prodSlots: 2, devSlots: 2, subpath: "project", expectedIDs: []string{"prod-deployment", "prod-branch-deployment", "dev-deployment", "dev-branch-deployment"}},
+ {name: "shared configuration with production slots", prodSlots: 4, devSlots: 2, subpath: "project", expectedIDs: []string{"prod-deployment", "prod-branch-deployment", "dev-deployment", "dev-branch-deployment"}},
+ {name: "shared configuration with development slots", prodSlots: 2, devSlots: 4, subpath: "project", expectedIDs: []string{"prod-deployment", "prod-branch-deployment", "dev-deployment", "dev-branch-deployment"}},
+ }
+ for _, tt := range tests {
+ for _, status := range []database.DeploymentStatus{database.DeploymentStatusRunning, database.DeploymentStatusStopped} {
+ t.Run(tt.name+"/"+status.String(), func(t *testing.T) {
+ project := &database.Project{ID: "project", PrimaryBranch: "main", ProdSlots: 2, DevSlots: 2}
+ db := &slotUpdateDB{
+ project: project,
+ deployments: []*database.Deployment{
+ {ID: "prod-deployment", Environment: "prod", Branch: "main", DesiredStatus: status},
+ {ID: "prod-branch-deployment", Environment: "prod", Branch: "release", DesiredStatus: status},
+ {ID: "dev-deployment", Environment: "dev", Branch: "feature", DesiredStatus: status},
+ {ID: "dev-branch-deployment", Environment: "dev", Branch: "another-feature", DesiredStatus: status},
+ },
+ desiredStatuses: make(map[string]database.DeploymentStatus),
+ }
+ jobClient := &slotUpdateJobs{}
+ svc := &Service{DB: db, Jobs: jobClient, Logger: zap.NewNop()}
+
+ updated, err := svc.UpdateProject(t.Context(), project, &database.UpdateProjectOptions{
+ PrimaryBranch: "main", ProdSlots: tt.prodSlots, DevSlots: tt.devSlots, Subpath: tt.subpath,
+ })
+ require.NoError(t, err)
+ require.Equal(t, tt.devSlots, updated.DevSlots)
+ require.Equal(t, tt.prodSlots, updated.ProdSlots)
+ require.Equal(t, tt.subpath, updated.Subpath)
+ require.ElementsMatch(t, tt.expectedIDs, jobClient.deploymentIDs)
+ expectedStatuses := make(map[string]database.DeploymentStatus)
+ for _, id := range tt.expectedIDs {
+ expectedStatuses[id] = status
+ }
+ // Unaffected deployments must not be touched, and hibernated deployments must stay stopped.
+ require.Equal(t, expectedStatuses, db.desiredStatuses)
+ })
+ }
+ }
+}
+
+// Only the database and queue operations used by UpdateProject are implemented.
+type slotUpdateDB struct {
+ database.DB
+ mu sync.Mutex
+ project *database.Project
+ deployments []*database.Deployment
+ desiredStatuses map[string]database.DeploymentStatus
+}
+
+func (db *slotUpdateDB) FindDeploymentsForProject(_ context.Context, _, environment, branch string) ([]*database.Deployment, error) {
+ var deployments []*database.Deployment
+ for _, d := range db.deployments {
+ if (environment == "" || d.Environment == environment) && (branch == "" || d.Branch == branch) {
+ deployments = append(deployments, d)
+ }
+ }
+ return deployments, nil
+}
+
+func (db *slotUpdateDB) UpdateProject(_ context.Context, _ string, opts *database.UpdateProjectOptions) (*database.Project, error) {
+ updated := *db.project
+ updated.ProdSlots = opts.ProdSlots
+ updated.DevSlots = opts.DevSlots
+ updated.Subpath = opts.Subpath
+ return &updated, nil
+}
+
+func (db *slotUpdateDB) UpdateDeploymentDesiredStatus(_ context.Context, id string, desired database.DeploymentStatus) (*database.Deployment, error) {
+ db.mu.Lock()
+ defer db.mu.Unlock()
+ db.desiredStatuses[id] = desired
+ return &database.Deployment{ID: id, DesiredStatus: desired}, nil
+}
+
+type slotUpdateJobs struct {
+ jobs.Client
+ mu sync.Mutex
+ deploymentIDs []string
+}
+
+func (j *slotUpdateJobs) ReconcileDeployment(_ context.Context, id string) (*jobs.InsertResult, error) {
+ j.mu.Lock()
+ defer j.mu.Unlock()
+ j.deploymentIDs = append(j.deploymentIDs, id)
+ return &jobs.InsertResult{}, nil
+}
diff --git a/admin/projects.go b/admin/projects.go
index ea4aa9479657..a8dc83aa29cb 100644
--- a/admin/projects.go
+++ b/admin/projects.go
@@ -195,8 +195,7 @@ func (s *Service) UpdateProject(ctx context.Context, oldProj *database.Project,
return nil, err
}
- impactsDeployments := (oldProj.ProdVersion != opts.ProdVersion) ||
- (oldProj.ProdSlots != opts.ProdSlots) ||
+ impactsAllDeployments := (oldProj.ProdVersion != opts.ProdVersion) ||
(oldProj.Name != opts.Name) ||
(oldProj.Subpath != opts.Subpath) ||
(oldProj.PrimaryBranch != opts.PrimaryBranch) ||
@@ -206,7 +205,9 @@ func (s *Service) UpdateProject(ctx context.Context, oldProj *database.Project,
!reflect.DeepEqual(oldProj.ArchiveAssetID, opts.ArchiveAssetID) ||
!reflect.DeepEqual(oldProj.OverrideDiskGB, opts.OverrideDiskGB)
- if !impactsDeployments {
+ prodSlotsChanged := oldProj.ProdSlots != opts.ProdSlots
+ devSlotsChanged := oldProj.DevSlots != opts.DevSlots
+ if !impactsAllDeployments && !prodSlotsChanged && !devSlotsChanged {
return proj, nil
}
@@ -246,9 +247,17 @@ func (s *Service) UpdateProject(ctx context.Context, oldProj *database.Project,
}
}
- // TODO: changing prod related fields like slots, branch etc should only impact prod deployments, but for now we update all deployments
- // NOTE: there is no way to change dev-slots right now
- err = s.UpdateDeploymentsForProject(ctx, proj)
+ // Slot-only changes should only reconcile deployments in the affected environment.
+ // An empty environment updates all deployments for shared changes or changes to both slot counts.
+ environment := ""
+ if !impactsAllDeployments {
+ if prodSlotsChanged && !devSlotsChanged {
+ environment = "prod"
+ } else if devSlotsChanged && !prodSlotsChanged {
+ environment = "dev"
+ }
+ }
+ err = s.UpdateDeploymentsForProject(ctx, proj, environment)
if err != nil {
return nil, err
}
@@ -294,7 +303,7 @@ func (s *Service) UpdateProjectVariables(ctx context.Context, project *database.
// Update deployments
s.Logger.Info("update project variables: updating deployments", observability.ZapCtx(ctx))
- err = s.UpdateDeploymentsForProject(ctx, project)
+ err = s.UpdateDeploymentsForProject(ctx, project, "")
if err != nil {
return err
}
@@ -315,7 +324,7 @@ func (s *Service) UpdateOrgDeploymentAnnotations(ctx context.Context, org *datab
}
for _, proj := range projs {
- err := s.UpdateDeploymentsForProject(ctx, proj)
+ err := s.UpdateDeploymentsForProject(ctx, proj, "")
if err != nil {
return err
}
diff --git a/admin/server/projects.go b/admin/server/projects.go
index 281680ec76ad..50a31e3585a6 100644
--- a/admin/server/projects.go
+++ b/admin/server/projects.go
@@ -32,9 +32,9 @@ const prodDeplTTL = 14 * 24 * time.Hour
// defaultProdSlots and defaultDevSlots are the slot counts applied when a CreateProject
// request omits them (e.g. the UI, or older CLIs that don't pass these fields).
-const defaultProdSlots = 4
+const defaultProdSlots = 2
-const defaultDevSlots = 4
+const defaultDevSlots = 2
// runtimeAccessTokenTTL is the validity duration of JWTs issued for runtime access when calling GetProject.
// This TTL is not used for tokens created for internal communication between the admin and runtime services.
diff --git a/admin/server/projects_test.go b/admin/server/projects_test.go
index 4b123ab2770a..09ec49d8932f 100644
--- a/admin/server/projects_test.go
+++ b/admin/server/projects_test.go
@@ -9,6 +9,34 @@ import (
"github.com/stretchr/testify/require"
)
+func TestCreateProjectSlots(t *testing.T) {
+ fix := testadmin.New(t)
+ _, client := fix.NewUser(t)
+ org, err := client.CreateOrganization(t.Context(), &adminv1.CreateOrganizationRequest{Name: randomName()})
+ require.NoError(t, err)
+
+ for _, tt := range []struct {
+ name string
+ prod, dev int64
+ wantProd, wantDev int64
+ }{
+ {name: "defaults", wantProd: 2, wantDev: 2},
+ {name: "explicit", prod: 1, dev: 3, wantProd: 1, wantDev: 3},
+ {name: "default-prod", dev: 1, wantProd: 2, wantDev: 1},
+ {name: "default-dev", prod: 3, wantProd: 3, wantDev: 2},
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ resp, err := client.CreateProject(t.Context(), &adminv1.CreateProjectRequest{
+ Org: org.Organization.Name, Project: tt.name,
+ ProdSlots: tt.prod, DevSlots: tt.dev, SkipDeploy: true,
+ })
+ require.NoError(t, err)
+ require.Equal(t, tt.wantProd, resp.Project.ProdSlots)
+ require.Equal(t, tt.wantDev, resp.Project.DevSlots)
+ })
+ }
+}
+
func TestProjectVariables(t *testing.T) {
fix := testadmin.New(t)
diff --git a/cli/pkg/local/slots.go b/cli/pkg/local/slots.go
index 889960ff4fb7..93b1a14e54d6 100644
--- a/cli/pkg/local/slots.go
+++ b/cli/pkg/local/slots.go
@@ -5,7 +5,7 @@ import "github.com/rilldata/rill/cli/pkg/cmdutil"
// DefaultProdSlots returns the prod slot count to request from the admin API.
//
// Returns 0 for released builds, which signals the admin server to apply its own default.
-// Dev builds request a single slot to keep local deployments small.
+// Dev builds request two slots to match the default for new deployments.
//
// A slot represents the following resources:
// - 1 CPU core
@@ -13,7 +13,7 @@ import "github.com/rilldata/rill/cli/pkg/cmdutil"
// - 40 GB of storage
func DefaultProdSlots(ch *cmdutil.Helper) int {
if ch.IsDev() {
- return 1
+ return 2
}
return 0
}
@@ -21,7 +21,7 @@ func DefaultProdSlots(ch *cmdutil.Helper) int {
// DefaultDevSlots returns the dev slot count to request from the admin API.
//
// Returns 0 for released builds, which signals the admin server to apply its own default.
-// Dev builds request a single slot to keep local deployments small.
+// Dev builds request two slots to match the default for new deployments.
//
// A slot represents the following resources:
// - 1 CPU core
@@ -29,7 +29,7 @@ func DefaultProdSlots(ch *cmdutil.Helper) int {
// - 40 GB of storage
func DefaultDevSlots(ch *cmdutil.Helper) int {
if ch.IsDev() {
- return 1
+ return 2
}
return 0
}
diff --git a/package-lock.json b/package-lock.json
index 5e663ba0c277..66f5d52b4b45 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -36176,10 +36176,12 @@
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.55.0",
"@tanstack/svelte-query": "~5.76.0",
+ "@testing-library/svelte": "^5.2.7",
"autoprefixer": "^10.4.20",
"axios": "1.13.6",
"cronstrue": "^2.41.0",
"dotenv": "^16.5.0",
+ "jsdom": "^26.0.0",
"orval": "^7.8.0",
"postcss": "^8.4.47",
"svelte": "^5.0.0",
diff --git a/web-admin/package.json b/web-admin/package.json
index 136ac21d5414..fa15018e8c94 100644
--- a/web-admin/package.json
+++ b/web-admin/package.json
@@ -24,17 +24,19 @@
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.55.0",
"@tanstack/svelte-query": "~5.76.0",
- "tanstack-table-8-svelte-5": "^0.1.2",
+ "@testing-library/svelte": "^5.2.7",
"autoprefixer": "^10.4.20",
"axios": "1.13.6",
"cronstrue": "^2.41.0",
"dotenv": "^16.5.0",
+ "jsdom": "^26.0.0",
"orval": "^7.8.0",
"postcss": "^8.4.47",
"svelte": "^5.0.0",
"svelte-check": "^4.0.4",
"svelte-preprocess": "^6.0.3",
"tailwindcss": "^3.4.13",
+ "tanstack-table-8-svelte-5": "^0.1.2",
"vitest": "^4.1.1",
"web-common": "*"
},
diff --git a/web-admin/src/features/bookmarks/utils.spec.ts b/web-admin/src/features/bookmarks/utils.spec.ts
index 245ba44d695b..a4691aa333a1 100644
--- a/web-admin/src/features/bookmarks/utils.spec.ts
+++ b/web-admin/src/features/bookmarks/utils.spec.ts
@@ -24,6 +24,7 @@ import {
AD_BIDS_TIME_RANGE_SUMMARY,
} from "@rilldata/web-common/features/dashboards/stores/test-data/data.ts";
import { getTimeControlState } from "@rilldata/web-common/features/dashboards/time-controls/time-control-store.ts";
+import { cleanUrlParams } from "@rilldata/web-common/features/dashboards/url-state/clean-url-params.ts";
import { convertPartialExploreStateToUrlParams } from "@rilldata/web-common/features/dashboards/url-state/convert-partial-explore-state-to-url-params.ts";
import type { DashboardTimeControls } from "@rilldata/web-common/lib/time/types.ts";
import { DashboardState_ActivePage } from "@rilldata/web-common/proto/gen/rill/ui/v1/dashboard_pb.ts";
@@ -67,7 +68,7 @@ describe("getBookmarkData and parseBookmarks", () => {
]),
selectedTimeRange: {
name: "P7D",
- interval: V1TimeGrain.TIME_GRAIN_HOUR, // Equal to default
+ interval: V1TimeGrain.TIME_GRAIN_HOUR, // Explicit grain for the P7D range
} as DashboardTimeControls,
showTimeComparison: true,
selectedComparisonTimeRange: {
@@ -84,23 +85,23 @@ describe("getBookmarkData and parseBookmarks", () => {
subTitle: "Empty url",
curUrlSearch: "",
expectedFullUrlSearch:
- "view=explore&tr=P7D&tz=UTC&grain=hour&compare_tr=rill-PP&f=publisher+IN+('Facebook','Yahoo')&compare_dim=&measures=bid_price&dims=*&expand_dim=domain&sort_by=bid_price&sort_dir=DESC&sort_type=value&leaderboard_measures=bid_price",
+ "view=explore&tr=P7D&tz=UTC&grain=hour&compare_tr=rill-PP&f=publisher+IN+('Facebook','Yahoo')&compare_dim=&measures=bid_price&dims=*&expand_dim=domain&sort_by=bid_price&sort_dir=DESC&sort_type=value&leaderboard_measures=bid_price&chart_type=adaptive&dyn_y=false&lb_ctx=false",
isActive: false,
},
{
subTitle: "Filter only equal",
curUrlSearch:
- "view=tdd&tr=P7D&compare_tr=rill-PW&f=publisher+IN+('Facebook','Yahoo')&measure=impressions&chart_type=stacked_bar",
+ "view=tdd&tr=P7D&grain=hour&compare_tr=rill-PW&f=publisher+IN+('Facebook','Yahoo')&measure=impressions&chart_type=stacked_bar",
expectedFullUrlSearch:
- "view=explore&tr=P7D&tz=UTC&grain=hour&compare_tr=rill-PP&f=publisher+IN+('Facebook','Yahoo')&compare_dim=&measures=bid_price&dims=*&expand_dim=domain&sort_by=bid_price&sort_dir=DESC&sort_type=value&leaderboard_measures=bid_price",
+ "view=explore&tr=P7D&tz=UTC&grain=hour&compare_tr=rill-PP&f=publisher+IN+('Facebook','Yahoo')&compare_dim=&measures=bid_price&dims=*&expand_dim=domain&sort_by=bid_price&sort_dir=DESC&sort_type=value&leaderboard_measures=bid_price&chart_type=adaptive&dyn_y=false&lb_ctx=false",
isActive: false,
},
{
subTitle: "Same url",
curUrlSearch:
- "tr=P7D&compare_tr=rill-PP&f=publisher+IN+('Facebook','Yahoo')&measures=bid_price&expand_dim=domain&sort_by=bid_price&leaderboard_measures=bid_price",
+ "tr=P7D&compare_tr=rill-PP&grain=hour&f=publisher+IN+('Facebook','Yahoo')&measures=bid_price&expand_dim=domain&sort_by=bid_price&leaderboard_measures=bid_price",
expectedFullUrlSearch:
- "view=explore&tr=P7D&tz=UTC&grain=hour&compare_tr=rill-PP&f=publisher+IN+('Facebook','Yahoo')&compare_dim=&measures=bid_price&dims=*&expand_dim=domain&sort_by=bid_price&sort_dir=DESC&sort_type=value&leaderboard_measures=bid_price",
+ "view=explore&tr=P7D&tz=UTC&grain=hour&compare_tr=rill-PP&f=publisher+IN+('Facebook','Yahoo')&compare_dim=&measures=bid_price&dims=*&expand_dim=domain&sort_by=bid_price&sort_dir=DESC&sort_type=value&leaderboard_measures=bid_price&chart_type=adaptive&dyn_y=false&lb_ctx=false",
isActive: true,
},
],
@@ -139,7 +140,7 @@ describe("getBookmarkData and parseBookmarks", () => {
{
subTitle: "Filter only equal",
curUrlSearch:
- "view=tdd&tr=P7D&compare_tr=rill-PW&f=publisher+IN+('Facebook','Yahoo')&measure=impressions&chart_type=stacked_bar",
+ "view=tdd&tr=P7D&grain=hour&compare_tr=rill-PW&f=publisher+IN+('Facebook','Yahoo')&measure=impressions&chart_type=stacked_bar",
expectedFullUrlSearch:
"view=tdd&tr=P7D&grain=hour&compare_tr=rill-PW&f=publisher+IN+('Facebook','Yahoo')&measure=impressions&chart_type=stacked_bar",
isActive: true,
@@ -147,9 +148,9 @@ describe("getBookmarkData and parseBookmarks", () => {
{
subTitle: "Same url",
curUrlSearch:
- "tr=P7D&compare_tr=rill-PP&f=publisher+IN+('Facebook','Yahoo')&measures=bid_price&expand_dim=domain&sort_by=bid_price&leaderboard_measures=bid_price",
+ "tr=P7D&compare_tr=rill-PP&grain=hour&f=publisher+IN+('Facebook','Yahoo')&measures=bid_price&expand_dim=domain&sort_by=bid_price&leaderboard_measures=bid_price",
expectedFullUrlSearch:
- "tr=P7D&grain=hour&compare_tr=rill-PP&f=publisher+IN+('Facebook','Yahoo')&measures=bid_price&expand_dim=domain&sort_by=bid_price&leaderboard_measures=bid_price",
+ "tr=P7D&compare_tr=rill-PP&grain=hour&f=publisher+IN+('Facebook','Yahoo')&measures=bid_price&expand_dim=domain&sort_by=bid_price&leaderboard_measures=bid_price",
isActive: true,
},
],
@@ -226,9 +227,17 @@ describe("getBookmarkData and parseBookmarks", () => {
expect(parsedBookmark.isActive).toEqual(isActive);
assertUnorderedUrlSearch(parsedProtoBookmark.url, parsedBookmark.url);
+ // Legacy proto bookmarks omit newer default-valued settings (dyn_y and lb_ctx).
+ // Compare the complete navigation URLs after removing equivalent defaults.
assertUnorderedUrlSearch(
- parsedProtoBookmark.fullUrl,
- parsedBookmark.fullUrl,
+ cleanUrlParams(
+ new URLSearchParams(parsedProtoBookmark.fullUrl.slice(1)),
+ AD_BIDS_RILL_DEFAULT_EXPLORE_URL_PARAMS,
+ ).toString(),
+ cleanUrlParams(
+ new URLSearchParams(parsedBookmark.fullUrl.slice(1)),
+ AD_BIDS_RILL_DEFAULT_EXPLORE_URL_PARAMS,
+ ).toString(),
);
expect(parsedProtoBookmark.isActive).toEqual(isActive);
});
diff --git a/web-admin/src/features/branches/BranchesSection.svelte b/web-admin/src/features/branches/BranchesSection.svelte
index aaa2057b59ea..2ccedf51e101 100644
--- a/web-admin/src/features/branches/BranchesSection.svelte
+++ b/web-admin/src/features/branches/BranchesSection.svelte
@@ -468,13 +468,15 @@
{/each}
-
-
-
- {m.branch_add_from_cli()}
-
-
-
+ {#if $cloudEditing}
+
+
+
+ {m.branch_add_from_cli()}
+
+
+
+ {/if}
{/if}
diff --git a/web-admin/src/features/branches/ProjectSlotsSection.svelte b/web-admin/src/features/branches/ProjectSlotsSection.svelte
new file mode 100644
index 000000000000..8b65c101ebc8
--- /dev/null
+++ b/web-admin/src/features/branches/ProjectSlotsSection.svelte
@@ -0,0 +1,11 @@
+
+
+
diff --git a/web-admin/src/features/projects/settings/HibernateProject.svelte b/web-admin/src/features/projects/settings/HibernateProject.svelte
index 93b18221d522..7295e6e9b2e4 100644
--- a/web-admin/src/features/projects/settings/HibernateProject.svelte
+++ b/web-admin/src/features/projects/settings/HibernateProject.svelte
@@ -3,6 +3,8 @@
createAdminServiceGetProject,
createAdminServiceHibernateProject,
getAdminServiceGetProjectQueryKey,
+ getAdminServiceListDeploymentsQueryKey,
+ getAdminServiceListProjectsForOrganizationQueryKey,
type RpcStatus,
} from "@rilldata/web-admin/client";
import SettingsContainer from "@rilldata/web-admin/features/organizations/settings/SettingsContainer.svelte";
@@ -33,9 +35,14 @@
);
let isHibernated = $derived(!$projectResp.data?.deployment);
+ let canManage = $derived(
+ !!$projectResp.data?.projectPermissions?.manageProject,
+ );
+
let hibernateResult = $derived($hibernateProjectMutation);
async function hibernateProject() {
+ if (!canManage || isHibernated || hibernateResult.isPending) return;
try {
await $hibernateProjectMutation.mutateAsync({
org: organization,
@@ -44,9 +51,21 @@
dialogOpen = false;
- await queryClient.refetchQueries({
- queryKey: getAdminServiceGetProjectQueryKey(organization, project),
- });
+ await Promise.all([
+ queryClient.invalidateQueries({
+ queryKey: getAdminServiceGetProjectQueryKey(organization, project),
+ }),
+ queryClient.invalidateQueries({
+ queryKey: getAdminServiceListDeploymentsQueryKey(
+ organization,
+ project,
+ ),
+ }),
+ queryClient.invalidateQueries({
+ queryKey:
+ getAdminServiceListProjectsForOrganizationQueryKey(organization),
+ }),
+ ]);
eventBus.emit("notification", {
message: m.settings_project_hibernated_notification(),
@@ -73,7 +92,7 @@
@@ -89,7 +108,11 @@
-