mirror of https://github.com/coder/coder.git
feat(coderd): allow workspace owners to mark workspaces as favorite (#11791)
- Adds column `favorite` to workspaces table - Adds API endpoints to favorite/unfavorite workspaces - Modifies sorting order to return owners' favorite workspaces first
This commit is contained in:
parent
6145da8a9e
commit
f92336c4d5
|
@ -61,6 +61,7 @@
|
|||
"failing_agents": []
|
||||
},
|
||||
"automatic_updates": "never",
|
||||
"allow_renames": false
|
||||
"allow_renames": false,
|
||||
"favorite": false
|
||||
}
|
||||
]
|
||||
|
|
|
@ -6999,6 +6999,62 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/favorite": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Workspaces"
|
||||
],
|
||||
"summary": "Favorite workspace by ID.",
|
||||
"operationId": "favorite-workspace-by-id",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Workspace ID",
|
||||
"name": "workspace",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Workspaces"
|
||||
],
|
||||
"summary": "Unfavorite workspace by ID.",
|
||||
"operationId": "unfavorite-workspace-by-id",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Workspace ID",
|
||||
"name": "workspace",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/resolve-autostart": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -11926,6 +11982,9 @@ const docTemplate = `{
|
|||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"favorite": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"health": {
|
||||
"description": "Health shows the health of the workspace and information about\nwhat is causing an unhealthy status.",
|
||||
"allOf": [
|
||||
|
|
|
@ -6175,6 +6175,58 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/favorite": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"tags": ["Workspaces"],
|
||||
"summary": "Favorite workspace by ID.",
|
||||
"operationId": "favorite-workspace-by-id",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Workspace ID",
|
||||
"name": "workspace",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"tags": ["Workspaces"],
|
||||
"summary": "Unfavorite workspace by ID.",
|
||||
"operationId": "unfavorite-workspace-by-id",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Workspace ID",
|
||||
"name": "workspace",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/resolve-autostart": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -10810,6 +10862,9 @@
|
|||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"favorite": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"health": {
|
||||
"description": "Health shows the health of the workspace and information about\nwhat is causing an unhealthy status.",
|
||||
"allOf": [
|
||||
|
|
|
@ -950,6 +950,8 @@ func New(options *Options) *API {
|
|||
r.Get("/watch", api.watchWorkspace)
|
||||
r.Put("/extend", api.putExtendWorkspace)
|
||||
r.Put("/dormant", api.putWorkspaceDormant)
|
||||
r.Put("/favorite", api.putFavoriteWorkspace)
|
||||
r.Delete("/favorite", api.deleteFavoriteWorkspace)
|
||||
r.Put("/autoupdates", api.putWorkspaceAutoupdates)
|
||||
r.Get("/resolve-autostart", api.resolveAutostart)
|
||||
})
|
||||
|
|
|
@ -891,6 +891,13 @@ func (q *querier) DeleteTailnetTunnel(ctx context.Context, arg database.DeleteTa
|
|||
return q.db.DeleteTailnetTunnel(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) FavoriteWorkspace(ctx context.Context, id uuid.UUID) error {
|
||||
fetch := func(ctx context.Context, id uuid.UUID) (database.Workspace, error) {
|
||||
return q.db.GetWorkspaceByID(ctx, id)
|
||||
}
|
||||
return update(q.log, q.auth, fetch, q.db.FavoriteWorkspace)(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
|
||||
return fetch(q.log, q.auth, q.db.GetAPIKeyByID)(ctx, id)
|
||||
}
|
||||
|
@ -2509,6 +2516,13 @@ func (q *querier) UnarchiveTemplateVersion(ctx context.Context, arg database.Una
|
|||
return q.db.UnarchiveTemplateVersion(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UnfavoriteWorkspace(ctx context.Context, id uuid.UUID) error {
|
||||
fetch := func(ctx context.Context, id uuid.UUID) (database.Workspace, error) {
|
||||
return q.db.GetWorkspaceByID(ctx, id)
|
||||
}
|
||||
return update(q.log, q.auth, fetch, q.db.UnfavoriteWorkspace)(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKeyByIDParams) error {
|
||||
fetch := func(ctx context.Context, arg database.UpdateAPIKeyByIDParams) (database.APIKey, error) {
|
||||
return q.db.GetAPIKeyByID(ctx, arg.ID)
|
||||
|
|
|
@ -1578,6 +1578,16 @@ func (s *MethodTestSuite) TestWorkspace() {
|
|||
WorkspaceID: ws.ID,
|
||||
}).Asserts(ws, rbac.ActionUpdate).Returns()
|
||||
}))
|
||||
s.Run("FavoriteWorkspace", s.Subtest(func(db database.Store, check *expects) {
|
||||
u := dbgen.User(s.T(), db, database.User{})
|
||||
ws := dbgen.Workspace(s.T(), db, database.Workspace{OwnerID: u.ID})
|
||||
check.Args(ws.ID).Asserts(ws, rbac.ActionUpdate).Returns()
|
||||
}))
|
||||
s.Run("UnfavoriteWorkspace", s.Subtest(func(db database.Store, check *expects) {
|
||||
u := dbgen.User(s.T(), db, database.User{})
|
||||
ws := dbgen.Workspace(s.T(), db, database.Workspace{OwnerID: u.ID})
|
||||
check.Args(ws.ID).Asserts(ws, rbac.ActionUpdate).Returns()
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *MethodTestSuite) TestExtraMethods() {
|
||||
|
|
|
@ -359,6 +359,7 @@ func (q *FakeQuerier) convertToWorkspaceRowsNoLock(ctx context.Context, workspac
|
|||
DeletingAt: w.DeletingAt,
|
||||
Count: count,
|
||||
AutomaticUpdates: w.AutomaticUpdates,
|
||||
Favorite: w.Favorite,
|
||||
}
|
||||
|
||||
for _, t := range q.templates {
|
||||
|
@ -1315,6 +1316,25 @@ func (*FakeQuerier) DeleteTailnetTunnel(_ context.Context, arg database.DeleteTa
|
|||
return database.DeleteTailnetTunnelRow{}, ErrUnimplemented
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) FavoriteWorkspace(_ context.Context, arg uuid.UUID) error {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for i := 0; i < len(q.workspaces); i++ {
|
||||
if q.workspaces[i].ID != arg {
|
||||
continue
|
||||
}
|
||||
q.workspaces[i].Favorite = true
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetAPIKeyByID(_ context.Context, id string) (database.APIKey, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
@ -5984,6 +6004,26 @@ func (q *FakeQuerier) UnarchiveTemplateVersion(_ context.Context, arg database.U
|
|||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UnfavoriteWorkspace(_ context.Context, arg uuid.UUID) error {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for i := 0; i < len(q.workspaces); i++ {
|
||||
if q.workspaces[i].ID != arg {
|
||||
continue
|
||||
}
|
||||
q.workspaces[i].Favorite = false
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPIKeyByIDParams) error {
|
||||
if err := validateDatabaseType(arg); err != nil {
|
||||
return err
|
||||
|
@ -7713,7 +7753,15 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
|
|||
w1 := workspaces[i]
|
||||
w2 := workspaces[j]
|
||||
|
||||
// Order by: running first
|
||||
// Order by: favorite first
|
||||
if arg.RequesterID == w1.OwnerID && w1.Favorite {
|
||||
return true
|
||||
}
|
||||
if arg.RequesterID == w2.OwnerID && w2.Favorite {
|
||||
return false
|
||||
}
|
||||
|
||||
// Order by: running
|
||||
w1IsRunning := isRunning(preloadedWorkspaceBuilds[w1.ID], preloadedProvisionerJobs[w1.ID])
|
||||
w2IsRunning := isRunning(preloadedWorkspaceBuilds[w2.ID], preloadedProvisionerJobs[w2.ID])
|
||||
|
||||
|
@ -7726,12 +7774,12 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
|
|||
}
|
||||
|
||||
// Order by: usernames
|
||||
if w1.ID != w2.ID {
|
||||
return sort.StringsAreSorted([]string{preloadedUsers[w1.ID].Username, preloadedUsers[w2.ID].Username})
|
||||
if strings.Compare(preloadedUsers[w1.ID].Username, preloadedUsers[w2.ID].Username) < 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Order by: workspace names
|
||||
return sort.StringsAreSorted([]string{w1.Name, w2.Name})
|
||||
return strings.Compare(w1.Name, w2.Name) < 0
|
||||
})
|
||||
|
||||
beforePageCount := len(workspaces)
|
||||
|
|
|
@ -300,6 +300,13 @@ func (m metricsStore) DeleteTailnetTunnel(ctx context.Context, arg database.Dele
|
|||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) FavoriteWorkspace(ctx context.Context, arg uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.FavoriteWorkspace(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("FavoriteWorkspace").Observe(time.Since(start).Seconds())
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m metricsStore) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
|
||||
start := time.Now()
|
||||
apiKey, err := m.s.GetAPIKeyByID(ctx, id)
|
||||
|
@ -1614,6 +1621,13 @@ func (m metricsStore) UnarchiveTemplateVersion(ctx context.Context, arg database
|
|||
return r0
|
||||
}
|
||||
|
||||
func (m metricsStore) UnfavoriteWorkspace(ctx context.Context, arg uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UnfavoriteWorkspace(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UnfavoriteWorkspace").Observe(time.Since(start).Seconds())
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m metricsStore) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKeyByIDParams) error {
|
||||
start := time.Now()
|
||||
err := m.s.UpdateAPIKeyByID(ctx, arg)
|
||||
|
|
|
@ -500,6 +500,20 @@ func (mr *MockStoreMockRecorder) DeleteTailnetTunnel(arg0, arg1 any) *gomock.Cal
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTailnetTunnel", reflect.TypeOf((*MockStore)(nil).DeleteTailnetTunnel), arg0, arg1)
|
||||
}
|
||||
|
||||
// FavoriteWorkspace mocks base method.
|
||||
func (m *MockStore) FavoriteWorkspace(arg0 context.Context, arg1 uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FavoriteWorkspace", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// FavoriteWorkspace indicates an expected call of FavoriteWorkspace.
|
||||
func (mr *MockStoreMockRecorder) FavoriteWorkspace(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FavoriteWorkspace", reflect.TypeOf((*MockStore)(nil).FavoriteWorkspace), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetAPIKeyByID mocks base method.
|
||||
func (m *MockStore) GetAPIKeyByID(arg0 context.Context, arg1 string) (database.APIKey, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -3410,6 +3424,20 @@ func (mr *MockStoreMockRecorder) UnarchiveTemplateVersion(arg0, arg1 any) *gomoc
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnarchiveTemplateVersion", reflect.TypeOf((*MockStore)(nil).UnarchiveTemplateVersion), arg0, arg1)
|
||||
}
|
||||
|
||||
// UnfavoriteWorkspace mocks base method.
|
||||
func (m *MockStore) UnfavoriteWorkspace(arg0 context.Context, arg1 uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UnfavoriteWorkspace", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UnfavoriteWorkspace indicates an expected call of UnfavoriteWorkspace.
|
||||
func (mr *MockStoreMockRecorder) UnfavoriteWorkspace(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnfavoriteWorkspace", reflect.TypeOf((*MockStore)(nil).UnfavoriteWorkspace), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateAPIKeyByID mocks base method.
|
||||
func (m *MockStore) UpdateAPIKeyByID(arg0 context.Context, arg1 database.UpdateAPIKeyByIDParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
|
@ -1235,9 +1235,12 @@ CREATE TABLE workspaces (
|
|||
last_used_at timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL,
|
||||
dormant_at timestamp with time zone,
|
||||
deleting_at timestamp with time zone,
|
||||
automatic_updates automatic_updates DEFAULT 'never'::automatic_updates NOT NULL
|
||||
automatic_updates automatic_updates DEFAULT 'never'::automatic_updates NOT NULL,
|
||||
favorite boolean DEFAULT false NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN workspaces.favorite IS 'Favorite is true if the workspace owner has favorited the workspace.';
|
||||
|
||||
ALTER TABLE ONLY licenses ALTER COLUMN id SET DEFAULT nextval('licenses_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY provisioner_job_logs ALTER COLUMN id SET DEFAULT nextval('provisioner_job_logs_id_seq'::regclass);
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE ONLY workspaces DROP COLUMN favorite;
|
|
@ -0,0 +1,3 @@
|
|||
ALTER TABLE ONLY workspaces
|
||||
ADD COLUMN favorite boolean NOT NULL DEFAULT false;
|
||||
COMMENT ON COLUMN workspaces.favorite IS 'Favorite is true if the workspace owner has favorited the workspace.';
|
|
@ -373,6 +373,7 @@ func ConvertWorkspaceRows(rows []GetWorkspacesRow) []Workspace {
|
|||
DormantAt: r.DormantAt,
|
||||
DeletingAt: r.DeletingAt,
|
||||
AutomaticUpdates: r.AutomaticUpdates,
|
||||
Favorite: r.Favorite,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -226,6 +226,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
|
|||
arg.LastUsedBefore,
|
||||
arg.LastUsedAfter,
|
||||
arg.UsingActive,
|
||||
arg.RequesterID,
|
||||
arg.Offset,
|
||||
arg.Limit,
|
||||
)
|
||||
|
@ -251,6 +252,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
|
|||
&i.DormantAt,
|
||||
&i.DeletingAt,
|
||||
&i.AutomaticUpdates,
|
||||
&i.Favorite,
|
||||
&i.TemplateName,
|
||||
&i.TemplateVersionID,
|
||||
&i.TemplateVersionName,
|
||||
|
|
|
@ -2185,6 +2185,8 @@ type Workspace struct {
|
|||
DormantAt sql.NullTime `db:"dormant_at" json:"dormant_at"`
|
||||
DeletingAt sql.NullTime `db:"deleting_at" json:"deleting_at"`
|
||||
AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"`
|
||||
// Favorite is true if the workspace owner has favorited the workspace.
|
||||
Favorite bool `db:"favorite" json:"favorite"`
|
||||
}
|
||||
|
||||
type WorkspaceAgent struct {
|
||||
|
|
|
@ -75,6 +75,7 @@ type sqlcQuerier interface {
|
|||
DeleteTailnetClientSubscription(ctx context.Context, arg DeleteTailnetClientSubscriptionParams) error
|
||||
DeleteTailnetPeer(ctx context.Context, arg DeleteTailnetPeerParams) (DeleteTailnetPeerRow, error)
|
||||
DeleteTailnetTunnel(ctx context.Context, arg DeleteTailnetTunnelParams) (DeleteTailnetTunnelRow, error)
|
||||
FavoriteWorkspace(ctx context.Context, id uuid.UUID) error
|
||||
GetAPIKeyByID(ctx context.Context, id string) (APIKey, error)
|
||||
// there is no unique constraint on empty token names
|
||||
GetAPIKeyByName(ctx context.Context, arg GetAPIKeyByNameParams) (APIKey, error)
|
||||
|
@ -321,6 +322,7 @@ type sqlcQuerier interface {
|
|||
TryAcquireLock(ctx context.Context, pgTryAdvisoryXactLock int64) (bool, error)
|
||||
// This will always work regardless of the current state of the template version.
|
||||
UnarchiveTemplateVersion(ctx context.Context, arg UnarchiveTemplateVersionParams) error
|
||||
UnfavoriteWorkspace(ctx context.Context, id uuid.UUID) error
|
||||
UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error
|
||||
UpdateExternalAuthLink(ctx context.Context, arg UpdateExternalAuthLinkParams) (ExternalAuthLink, error)
|
||||
UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyParams) (GitSSHKey, error)
|
||||
|
|
|
@ -10849,6 +10849,15 @@ func (q *sqlQuerier) BatchUpdateWorkspaceLastUsedAt(ctx context.Context, arg Bat
|
|||
return err
|
||||
}
|
||||
|
||||
const favoriteWorkspace = `-- name: FavoriteWorkspace :exec
|
||||
UPDATE workspaces SET favorite = true WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) FavoriteWorkspace(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := q.db.ExecContext(ctx, favoriteWorkspace, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getDeploymentWorkspaceStats = `-- name: GetDeploymentWorkspaceStats :one
|
||||
WITH workspaces_with_jobs AS (
|
||||
SELECT
|
||||
|
@ -10935,7 +10944,7 @@ func (q *sqlQuerier) GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploy
|
|||
|
||||
const getWorkspaceByAgentID = `-- name: GetWorkspaceByAgentID :one
|
||||
SELECT
|
||||
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates,
|
||||
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite,
|
||||
templates.name as template_name
|
||||
FROM
|
||||
workspaces
|
||||
|
@ -10989,6 +10998,7 @@ func (q *sqlQuerier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUI
|
|||
&i.Workspace.DormantAt,
|
||||
&i.Workspace.DeletingAt,
|
||||
&i.Workspace.AutomaticUpdates,
|
||||
&i.Workspace.Favorite,
|
||||
&i.TemplateName,
|
||||
)
|
||||
return i, err
|
||||
|
@ -10996,7 +11006,7 @@ func (q *sqlQuerier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUI
|
|||
|
||||
const getWorkspaceByID = `-- name: GetWorkspaceByID :one
|
||||
SELECT
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
|
@ -11023,13 +11033,14 @@ func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Worksp
|
|||
&i.DormantAt,
|
||||
&i.DeletingAt,
|
||||
&i.AutomaticUpdates,
|
||||
&i.Favorite,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getWorkspaceByOwnerIDAndName = `-- name: GetWorkspaceByOwnerIDAndName :one
|
||||
SELECT
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
|
@ -11063,13 +11074,14 @@ func (q *sqlQuerier) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWo
|
|||
&i.DormantAt,
|
||||
&i.DeletingAt,
|
||||
&i.AutomaticUpdates,
|
||||
&i.Favorite,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getWorkspaceByWorkspaceAppID = `-- name: GetWorkspaceByWorkspaceAppID :one
|
||||
SELECT
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite
|
||||
FROM
|
||||
workspaces
|
||||
WHERE
|
||||
|
@ -11122,6 +11134,7 @@ func (q *sqlQuerier) GetWorkspaceByWorkspaceAppID(ctx context.Context, workspace
|
|||
&i.DormantAt,
|
||||
&i.DeletingAt,
|
||||
&i.AutomaticUpdates,
|
||||
&i.Favorite,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
@ -11166,7 +11179,7 @@ func (q *sqlQuerier) GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx context.Conte
|
|||
|
||||
const getWorkspaces = `-- name: GetWorkspaces :many
|
||||
SELECT
|
||||
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates,
|
||||
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite,
|
||||
COALESCE(template.name, 'unknown') as template_name,
|
||||
latest_build.template_version_id,
|
||||
latest_build.template_version_name,
|
||||
|
@ -11355,6 +11368,8 @@ WHERE
|
|||
-- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces
|
||||
-- @authorize_filter
|
||||
ORDER BY
|
||||
-- To ensure that 'favorite' workspaces show up first in the list only for their owner.
|
||||
CASE WHEN workspaces.owner_id = $14 AND workspaces.favorite THEN 0 ELSE 1 END ASC,
|
||||
(latest_build.completed_at IS NOT NULL AND
|
||||
latest_build.canceled_at IS NULL AND
|
||||
latest_build.error IS NULL AND
|
||||
|
@ -11363,11 +11378,11 @@ ORDER BY
|
|||
LOWER(workspaces.name) ASC
|
||||
LIMIT
|
||||
CASE
|
||||
WHEN $15 :: integer > 0 THEN
|
||||
$15
|
||||
WHEN $16 :: integer > 0 THEN
|
||||
$16
|
||||
END
|
||||
OFFSET
|
||||
$14
|
||||
$15
|
||||
`
|
||||
|
||||
type GetWorkspacesParams struct {
|
||||
|
@ -11384,6 +11399,7 @@ type GetWorkspacesParams struct {
|
|||
LastUsedBefore time.Time `db:"last_used_before" json:"last_used_before"`
|
||||
LastUsedAfter time.Time `db:"last_used_after" json:"last_used_after"`
|
||||
UsingActive sql.NullBool `db:"using_active" json:"using_active"`
|
||||
RequesterID uuid.UUID `db:"requester_id" json:"requester_id"`
|
||||
Offset int32 `db:"offset_" json:"offset_"`
|
||||
Limit int32 `db:"limit_" json:"limit_"`
|
||||
}
|
||||
|
@ -11403,6 +11419,7 @@ type GetWorkspacesRow struct {
|
|||
DormantAt sql.NullTime `db:"dormant_at" json:"dormant_at"`
|
||||
DeletingAt sql.NullTime `db:"deleting_at" json:"deleting_at"`
|
||||
AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"`
|
||||
Favorite bool `db:"favorite" json:"favorite"`
|
||||
TemplateName string `db:"template_name" json:"template_name"`
|
||||
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
||||
TemplateVersionName sql.NullString `db:"template_version_name" json:"template_version_name"`
|
||||
|
@ -11424,6 +11441,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams)
|
|||
arg.LastUsedBefore,
|
||||
arg.LastUsedAfter,
|
||||
arg.UsingActive,
|
||||
arg.RequesterID,
|
||||
arg.Offset,
|
||||
arg.Limit,
|
||||
)
|
||||
|
@ -11449,6 +11467,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams)
|
|||
&i.DormantAt,
|
||||
&i.DeletingAt,
|
||||
&i.AutomaticUpdates,
|
||||
&i.Favorite,
|
||||
&i.TemplateName,
|
||||
&i.TemplateVersionID,
|
||||
&i.TemplateVersionName,
|
||||
|
@ -11469,7 +11488,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams)
|
|||
|
||||
const getWorkspacesEligibleForTransition = `-- name: GetWorkspacesEligibleForTransition :many
|
||||
SELECT
|
||||
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates
|
||||
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite
|
||||
FROM
|
||||
workspaces
|
||||
LEFT JOIN
|
||||
|
@ -11557,6 +11576,7 @@ func (q *sqlQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, now
|
|||
&i.DormantAt,
|
||||
&i.DeletingAt,
|
||||
&i.AutomaticUpdates,
|
||||
&i.Favorite,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -11587,7 +11607,7 @@ INSERT INTO
|
|||
automatic_updates
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite
|
||||
`
|
||||
|
||||
type InsertWorkspaceParams struct {
|
||||
|
@ -11634,10 +11654,20 @@ func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspacePar
|
|||
&i.DormantAt,
|
||||
&i.DeletingAt,
|
||||
&i.AutomaticUpdates,
|
||||
&i.Favorite,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const unfavoriteWorkspace = `-- name: UnfavoriteWorkspace :exec
|
||||
UPDATE workspaces SET favorite = false WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) UnfavoriteWorkspace(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := q.db.ExecContext(ctx, unfavoriteWorkspace, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateTemplateWorkspacesLastUsedAt = `-- name: UpdateTemplateWorkspacesLastUsedAt :exec
|
||||
UPDATE workspaces
|
||||
SET
|
||||
|
@ -11664,7 +11694,7 @@ SET
|
|||
WHERE
|
||||
id = $1
|
||||
AND deleted = false
|
||||
RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates
|
||||
RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite
|
||||
`
|
||||
|
||||
type UpdateWorkspaceParams struct {
|
||||
|
@ -11690,6 +11720,7 @@ func (q *sqlQuerier) UpdateWorkspace(ctx context.Context, arg UpdateWorkspacePar
|
|||
&i.DormantAt,
|
||||
&i.DeletingAt,
|
||||
&i.AutomaticUpdates,
|
||||
&i.Favorite,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
@ -11776,7 +11807,7 @@ WHERE
|
|||
workspaces.id = $1
|
||||
AND templates.id = workspaces.template_id
|
||||
RETURNING
|
||||
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates
|
||||
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite
|
||||
`
|
||||
|
||||
type UpdateWorkspaceDormantDeletingAtParams struct {
|
||||
|
@ -11802,6 +11833,7 @@ func (q *sqlQuerier) UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg U
|
|||
&i.DormantAt,
|
||||
&i.DeletingAt,
|
||||
&i.AutomaticUpdates,
|
||||
&i.Favorite,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
@ -267,6 +267,8 @@ WHERE
|
|||
-- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces
|
||||
-- @authorize_filter
|
||||
ORDER BY
|
||||
-- To ensure that 'favorite' workspaces show up first in the list only for their owner.
|
||||
CASE WHEN workspaces.owner_id = @requester_id AND workspaces.favorite THEN 0 ELSE 1 END ASC,
|
||||
(latest_build.completed_at IS NOT NULL AND
|
||||
latest_build.canceled_at IS NULL AND
|
||||
latest_build.error IS NULL AND
|
||||
|
@ -552,3 +554,9 @@ SET
|
|||
automatic_updates = $2
|
||||
WHERE
|
||||
id = $1;
|
||||
|
||||
-- name: FavoriteWorkspace :exec
|
||||
UPDATE workspaces SET favorite = true WHERE id = @id;
|
||||
|
||||
-- name: UnfavoriteWorkspace :exec
|
||||
UPDATE workspaces SET favorite = false WHERE id = @id;
|
||||
|
|
|
@ -55,6 +55,7 @@ var (
|
|||
func (api *API) workspace(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
apiKey := httpmw.APIKey(r)
|
||||
|
||||
var (
|
||||
deletedStr = r.URL.Query().Get("include_deleted")
|
||||
|
@ -102,6 +103,7 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertWorkspace(
|
||||
apiKey.UserID,
|
||||
workspace,
|
||||
data.builds[0],
|
||||
data.templates[0],
|
||||
|
@ -157,6 +159,10 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// To show the requester's favorite workspaces first, we pass their userID and compare it to
|
||||
// the workspace owner_id when ordering the rows.
|
||||
filter.RequesterID = apiKey.UserID
|
||||
|
||||
workspaceRows, err := api.Database.GetAuthorizedWorkspaces(ctx, filter, prepared)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
|
@ -184,7 +190,7 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
wss, err := convertWorkspaces(workspaces, data)
|
||||
wss, err := convertWorkspaces(apiKey.UserID, workspaces, data)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error converting workspaces.",
|
||||
|
@ -213,6 +219,7 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request)
|
|||
ctx := r.Context()
|
||||
owner := httpmw.UserParam(r)
|
||||
workspaceName := chi.URLParam(r, "workspacename")
|
||||
apiKey := httpmw.APIKey(r)
|
||||
|
||||
includeDeleted := false
|
||||
if s := r.URL.Query().Get("include_deleted"); s != "" {
|
||||
|
@ -274,6 +281,7 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request)
|
|||
return
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertWorkspace(
|
||||
apiKey.UserID,
|
||||
workspace,
|
||||
data.builds[0],
|
||||
data.templates[0],
|
||||
|
@ -583,6 +591,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
|||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusCreated, convertWorkspace(
|
||||
apiKey.UserID,
|
||||
workspace,
|
||||
apiBuild,
|
||||
template,
|
||||
|
@ -854,6 +863,7 @@ func (api *API) putWorkspaceDormant(rw http.ResponseWriter, r *http.Request) {
|
|||
var (
|
||||
ctx = r.Context()
|
||||
workspace = httpmw.WorkspaceParam(r)
|
||||
apiKey = httpmw.APIKey(r)
|
||||
oldWorkspace = workspace
|
||||
auditor = api.Auditor.Load()
|
||||
aReq, commitAudit = audit.InitRequest[database.Workspace](rw, &audit.RequestParams{
|
||||
|
@ -922,6 +932,7 @@ func (api *API) putWorkspaceDormant(rw http.ResponseWriter, r *http.Request) {
|
|||
|
||||
aReq.New = workspace
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertWorkspace(
|
||||
apiKey.UserID,
|
||||
workspace,
|
||||
data.builds[0],
|
||||
data.templates[0],
|
||||
|
@ -1021,6 +1032,98 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
|
|||
httpapi.Write(ctx, rw, code, resp)
|
||||
}
|
||||
|
||||
// @Summary Favorite workspace by ID.
|
||||
// @ID favorite-workspace-by-id
|
||||
// @Security CoderSessionToken
|
||||
// @Tags Workspaces
|
||||
// @Param workspace path string true "Workspace ID" format(uuid)
|
||||
// @Success 204
|
||||
// @Router /workspaces/{workspace}/favorite [put]
|
||||
func (api *API) putFavoriteWorkspace(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
workspace = httpmw.WorkspaceParam(r)
|
||||
apiKey = httpmw.APIKey(r)
|
||||
auditor = api.Auditor.Load()
|
||||
)
|
||||
|
||||
if apiKey.UserID != workspace.OwnerID {
|
||||
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
|
||||
Message: "You can only favorite workspaces that you own.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
aReq, commitAudit := audit.InitRequest[database.Workspace](rw, &audit.RequestParams{
|
||||
Audit: *auditor,
|
||||
Log: api.Logger,
|
||||
Request: r,
|
||||
Action: database.AuditActionWrite,
|
||||
})
|
||||
defer commitAudit()
|
||||
aReq.Old = workspace
|
||||
|
||||
err := api.Database.FavoriteWorkspace(ctx, workspace.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error setting workspace as favorite",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
aReq.New = workspace
|
||||
aReq.New.Favorite = true
|
||||
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// @Summary Unfavorite workspace by ID.
|
||||
// @ID unfavorite-workspace-by-id
|
||||
// @Security CoderSessionToken
|
||||
// @Tags Workspaces
|
||||
// @Param workspace path string true "Workspace ID" format(uuid)
|
||||
// @Success 204
|
||||
// @Router /workspaces/{workspace}/favorite [delete]
|
||||
func (api *API) deleteFavoriteWorkspace(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
workspace = httpmw.WorkspaceParam(r)
|
||||
apiKey = httpmw.APIKey(r)
|
||||
auditor = api.Auditor.Load()
|
||||
)
|
||||
|
||||
if apiKey.UserID != workspace.OwnerID {
|
||||
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
|
||||
Message: "You can only un-favorite workspaces that you own.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
aReq, commitAudit := audit.InitRequest[database.Workspace](rw, &audit.RequestParams{
|
||||
Audit: *auditor,
|
||||
Log: api.Logger,
|
||||
Request: r,
|
||||
Action: database.AuditActionWrite,
|
||||
})
|
||||
|
||||
defer commitAudit()
|
||||
aReq.Old = workspace
|
||||
|
||||
err := api.Database.UnfavoriteWorkspace(ctx, workspace.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error unsetting workspace as favorite",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
aReq.New = workspace
|
||||
aReq.New.Favorite = false
|
||||
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// @Summary Update workspace automatic updates by ID
|
||||
// @ID update-workspace-automatic-updates-by-id
|
||||
// @Security CoderSessionToken
|
||||
|
@ -1186,6 +1289,7 @@ func (api *API) resolveAutostart(rw http.ResponseWriter, r *http.Request) {
|
|||
func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
apiKey := httpmw.APIKey(r)
|
||||
|
||||
sendEvent, senderClosed, err := httpapi.ServerSentEventSender(rw, r)
|
||||
if err != nil {
|
||||
|
@ -1248,6 +1352,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
|
|||
_ = sendEvent(ctx, codersdk.ServerSentEvent{
|
||||
Type: codersdk.ServerSentEventTypeData,
|
||||
Data: convertWorkspace(
|
||||
apiKey.UserID,
|
||||
workspace,
|
||||
data.builds[0],
|
||||
data.templates[0],
|
||||
|
@ -1366,7 +1471,7 @@ func (api *API) workspaceData(ctx context.Context, workspaces []database.Workspa
|
|||
}, nil
|
||||
}
|
||||
|
||||
func convertWorkspaces(workspaces []database.Workspace, data workspaceData) ([]codersdk.Workspace, error) {
|
||||
func convertWorkspaces(requesterID uuid.UUID, workspaces []database.Workspace, data workspaceData) ([]codersdk.Workspace, error) {
|
||||
buildByWorkspaceID := map[uuid.UUID]codersdk.WorkspaceBuild{}
|
||||
for _, workspaceBuild := range data.builds {
|
||||
buildByWorkspaceID[workspaceBuild.WorkspaceID] = workspaceBuild
|
||||
|
@ -1401,6 +1506,7 @@ func convertWorkspaces(workspaces []database.Workspace, data workspaceData) ([]c
|
|||
}
|
||||
|
||||
apiWorkspaces = append(apiWorkspaces, convertWorkspace(
|
||||
requesterID,
|
||||
workspace,
|
||||
build,
|
||||
template,
|
||||
|
@ -1412,6 +1518,7 @@ func convertWorkspaces(workspaces []database.Workspace, data workspaceData) ([]c
|
|||
}
|
||||
|
||||
func convertWorkspace(
|
||||
requesterID uuid.UUID,
|
||||
workspace database.Workspace,
|
||||
workspaceBuild codersdk.WorkspaceBuild,
|
||||
template database.Template,
|
||||
|
@ -1444,6 +1551,9 @@ func convertWorkspace(
|
|||
|
||||
ttlMillis := convertWorkspaceTTLMillis(workspace.Ttl)
|
||||
|
||||
// Only show favorite status if you own the workspace.
|
||||
requesterFavorite := workspace.OwnerID == requesterID && workspace.Favorite
|
||||
|
||||
return codersdk.Workspace{
|
||||
ID: workspace.ID,
|
||||
CreatedAt: workspace.CreatedAt,
|
||||
|
@ -1472,6 +1582,7 @@ func convertWorkspace(
|
|||
},
|
||||
AutomaticUpdates: codersdk.AutomaticUpdates(workspace.AutomaticUpdates),
|
||||
AllowRenames: allowRenames,
|
||||
Favorite: requesterFavorite,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -479,55 +479,85 @@ func TestAdminViewAllWorkspaces(t *testing.T) {
|
|||
func TestWorkspacesSortOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
client, db := coderdtest.NewWithDatabase(t, nil)
|
||||
firstUser := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, firstUser.OrganizationID, version.ID)
|
||||
secondUserClient, secondUser := coderdtest.CreateAnotherUserMutators(t, client, firstUser.OrganizationID, []string{"owner"}, func(r *codersdk.CreateUserRequest) {
|
||||
r.Username = "zzz"
|
||||
})
|
||||
|
||||
// c-workspace should be running
|
||||
workspace1 := coderdtest.CreateWorkspace(t, client, firstUser.OrganizationID, template.ID, func(ctr *codersdk.CreateWorkspaceRequest) {
|
||||
ctr.Name = "c-workspace"
|
||||
})
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace1.LatestBuild.ID)
|
||||
wsbC := dbfake.WorkspaceBuild(t, db, database.Workspace{Name: "c-workspace", OwnerID: firstUser.UserID, OrganizationID: firstUser.OrganizationID}).Do()
|
||||
|
||||
// b-workspace should be stopped
|
||||
workspace2 := coderdtest.CreateWorkspace(t, client, firstUser.OrganizationID, template.ID, func(ctr *codersdk.CreateWorkspaceRequest) {
|
||||
ctr.Name = "b-workspace"
|
||||
})
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace2.LatestBuild.ID)
|
||||
|
||||
build2 := coderdtest.CreateWorkspaceBuild(t, client, workspace2, database.WorkspaceTransitionStop)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build2.ID)
|
||||
wsbB := dbfake.WorkspaceBuild(t, db, database.Workspace{Name: "b-workspace", OwnerID: firstUser.UserID, OrganizationID: firstUser.OrganizationID}).Seed(database.WorkspaceBuild{Transition: database.WorkspaceTransitionStop}).Do()
|
||||
|
||||
// a-workspace should be running
|
||||
workspace3 := coderdtest.CreateWorkspace(t, client, firstUser.OrganizationID, template.ID, func(ctr *codersdk.CreateWorkspaceRequest) {
|
||||
ctr.Name = "a-workspace"
|
||||
})
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace3.LatestBuild.ID)
|
||||
wsbA := dbfake.WorkspaceBuild(t, db, database.Workspace{Name: "a-workspace", OwnerID: firstUser.UserID, OrganizationID: firstUser.OrganizationID}).Do()
|
||||
|
||||
// d-workspace should be stopped
|
||||
wsbD := dbfake.WorkspaceBuild(t, db, database.Workspace{Name: "d-workspace", OwnerID: secondUser.ID, OrganizationID: firstUser.OrganizationID}).Seed(database.WorkspaceBuild{Transition: database.WorkspaceTransitionStop}).Do()
|
||||
|
||||
// e-workspace should also be stopped
|
||||
wsbE := dbfake.WorkspaceBuild(t, db, database.Workspace{Name: "e-workspace", OwnerID: secondUser.ID, OrganizationID: firstUser.OrganizationID}).Seed(database.WorkspaceBuild{Transition: database.WorkspaceTransitionStop}).Do()
|
||||
|
||||
// f-workspace is also stopped, but is marked as favorite
|
||||
wsbF := dbfake.WorkspaceBuild(t, db, database.Workspace{Name: "f-workspace", OwnerID: firstUser.UserID, OrganizationID: firstUser.OrganizationID}).Seed(database.WorkspaceBuild{Transition: database.WorkspaceTransitionStop}).Do()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
require.NoError(t, client.FavoriteWorkspace(ctx, wsbF.Workspace.ID)) // need to do this via API call for now
|
||||
|
||||
workspacesResponse, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
||||
require.NoError(t, err, "(first) fetch workspaces")
|
||||
workspaces := workspacesResponse.Workspaces
|
||||
|
||||
expected := []string{
|
||||
workspace3.Name,
|
||||
workspace1.Name,
|
||||
workspace2.Name,
|
||||
expectedNames := []string{
|
||||
wsbF.Workspace.Name, // favorite
|
||||
wsbA.Workspace.Name, // running
|
||||
wsbC.Workspace.Name, // running
|
||||
wsbB.Workspace.Name, // stopped, testuser < zzz
|
||||
wsbD.Workspace.Name, // stopped, zzz > testuser
|
||||
wsbE.Workspace.Name, // stopped, zzz > testuser
|
||||
}
|
||||
|
||||
var actual []string
|
||||
actualNames := make([]string, 0, len(expectedNames))
|
||||
for _, w := range workspaces {
|
||||
actual = append(actual, w.Name)
|
||||
actualNames = append(actualNames, w.Name)
|
||||
}
|
||||
|
||||
// the correct sorting order is:
|
||||
// 1. Running workspaces
|
||||
// 2. Sort by usernames
|
||||
// 3. Sort by workspace names
|
||||
require.Equal(t, expected, actual)
|
||||
// 1. Favorite workspaces (we have one, workspace-f)
|
||||
// 2. Running workspaces
|
||||
// 3. Sort by usernames
|
||||
// 4. Sort by workspace names
|
||||
assert.Equal(t, expectedNames, actualNames)
|
||||
|
||||
// Once again but this time as a different user. This time we do not expect to see another
|
||||
// user's favorites first.
|
||||
workspacesResponse, err = secondUserClient.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
||||
require.NoError(t, err, "(second) fetch workspaces")
|
||||
workspaces = workspacesResponse.Workspaces
|
||||
|
||||
expectedNames = []string{
|
||||
wsbA.Workspace.Name, // running
|
||||
wsbC.Workspace.Name, // running
|
||||
wsbB.Workspace.Name, // stopped, testuser < zzz
|
||||
wsbF.Workspace.Name, // stopped, testuser < zzz
|
||||
wsbD.Workspace.Name, // stopped, zzz > testuser
|
||||
wsbE.Workspace.Name, // stopped, zzz > testuser
|
||||
}
|
||||
|
||||
actualNames = make([]string, 0, len(expectedNames))
|
||||
for _, w := range workspaces {
|
||||
actualNames = append(actualNames, w.Name)
|
||||
}
|
||||
|
||||
// the correct sorting order is:
|
||||
// 1. Favorite workspaces (we have none this time)
|
||||
// 2. Running workspaces
|
||||
// 3. Sort by usernames
|
||||
// 4. Sort by workspace names
|
||||
assert.Equal(t, expectedNames, actualNames)
|
||||
}
|
||||
|
||||
func TestPostWorkspacesByOrganization(t *testing.T) {
|
||||
|
@ -2978,3 +3008,85 @@ func TestWorkspaceDormant(t *testing.T) {
|
|||
coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStop, database.WorkspaceTransitionStart)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkspaceFavoriteUnfavorite(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Given:
|
||||
var (
|
||||
auditRecorder = audit.NewMock()
|
||||
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
||||
Auditor: auditRecorder,
|
||||
})
|
||||
owner = coderdtest.CreateFirstUser(t, client)
|
||||
memberClient, member = coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
// This will be our 'favorite' workspace
|
||||
wsb1 = dbfake.WorkspaceBuild(t, db, database.Workspace{OwnerID: member.ID, OrganizationID: owner.OrganizationID}).Do()
|
||||
wsb2 = dbfake.WorkspaceBuild(t, db, database.Workspace{OwnerID: owner.UserID, OrganizationID: owner.OrganizationID}).Do()
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
// Initially, workspace should not be favored for member.
|
||||
ws, err := memberClient.Workspace(ctx, wsb1.Workspace.ID)
|
||||
require.NoError(t, err)
|
||||
require.False(t, ws.Favorite)
|
||||
|
||||
// When user favorites workspace
|
||||
err = memberClient.FavoriteWorkspace(ctx, wsb1.Workspace.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then it should be favored for them.
|
||||
ws, err = memberClient.Workspace(ctx, wsb1.Workspace.ID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ws.Favorite)
|
||||
|
||||
// And it should be audited.
|
||||
require.True(t, auditRecorder.Contains(t, database.AuditLog{
|
||||
Action: database.AuditActionWrite,
|
||||
ResourceType: database.ResourceTypeWorkspace,
|
||||
ResourceTarget: wsb1.Workspace.Name,
|
||||
UserID: member.ID,
|
||||
}))
|
||||
auditRecorder.ResetLogs()
|
||||
|
||||
// This should not show for the owner.
|
||||
ws, err = client.Workspace(ctx, wsb1.Workspace.ID)
|
||||
require.NoError(t, err)
|
||||
require.False(t, ws.Favorite)
|
||||
|
||||
// When member unfavorites workspace
|
||||
err = memberClient.UnfavoriteWorkspace(ctx, wsb1.Workspace.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then it should no longer be favored
|
||||
ws, err = memberClient.Workspace(ctx, wsb1.Workspace.ID)
|
||||
require.NoError(t, err)
|
||||
require.False(t, ws.Favorite, "no longer favorite")
|
||||
|
||||
// And it should show in the audit logs.
|
||||
require.True(t, auditRecorder.Contains(t, database.AuditLog{
|
||||
Action: database.AuditActionWrite,
|
||||
ResourceType: database.ResourceTypeWorkspace,
|
||||
ResourceTarget: wsb1.Workspace.Name,
|
||||
UserID: member.ID,
|
||||
}))
|
||||
|
||||
// Users without write access to the workspace should not be able to perform the above.
|
||||
err = memberClient.FavoriteWorkspace(ctx, wsb2.Workspace.ID)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
|
||||
err = memberClient.UnfavoriteWorkspace(ctx, wsb2.Workspace.ID)
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
|
||||
|
||||
// You should not be able to favorite any workspace you do not own, even if you are the owner.
|
||||
err = client.FavoriteWorkspace(ctx, wsb1.Workspace.ID)
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusForbidden, sdkErr.StatusCode())
|
||||
|
||||
err = client.UnfavoriteWorkspace(ctx, wsb1.Workspace.ID)
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusForbidden, sdkErr.StatusCode())
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@ type Workspace struct {
|
|||
Health WorkspaceHealth `json:"health"`
|
||||
AutomaticUpdates AutomaticUpdates `json:"automatic_updates" enums:"always,never"`
|
||||
AllowRenames bool `json:"allow_renames"`
|
||||
Favorite bool `json:"favorite"`
|
||||
}
|
||||
|
||||
func (w Workspace) FullName() string {
|
||||
|
@ -471,6 +472,30 @@ func (c *Client) ResolveAutostart(ctx context.Context, workspaceID string) (Reso
|
|||
return response, json.NewDecoder(res.Body).Decode(&response)
|
||||
}
|
||||
|
||||
func (c *Client) FavoriteWorkspace(ctx context.Context, workspaceID uuid.UUID) error {
|
||||
res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/workspaces/%s/favorite", workspaceID), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusNoContent {
|
||||
return ReadBodyAsError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) UnfavoriteWorkspace(ctx context.Context, workspaceID uuid.UUID) error {
|
||||
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/workspaces/%s/favorite", workspaceID), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusNoContent {
|
||||
return ReadBodyAsError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WorkspaceNotifyChannel is the PostgreSQL NOTIFY
|
||||
// channel to listen for updates on. The payload is empty,
|
||||
// because the size of a workspace payload can be very large.
|
||||
|
|
|
@ -19,7 +19,7 @@ We track the following resources:
|
|||
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>active_version_id</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deprecated</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_ttl</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>require_active_version</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>use_max_ttl</td><td>true</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
|
||||
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>theme_preference</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
|
||||
| Workspace<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
| Workspace<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>favorite</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
|
||||
| WorkspaceProxy<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>derp_enabled</td><td>true</td></tr><tr><td>derp_only</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>region_id</td><td>true</td></tr><tr><td>token_hashed_secret</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>url</td><td>true</td></tr><tr><td>version</td><td>true</td></tr><tr><td>wildcard_hostname</td><td>true</td></tr></tbody></table> |
|
||||
|
||||
|
|
|
@ -5921,6 +5921,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
|||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"deleting_at": "2019-08-24T14:15:22Z",
|
||||
"dormant_at": "2019-08-24T14:15:22Z",
|
||||
"favorite": true,
|
||||
"health": {
|
||||
"failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"healthy": false
|
||||
|
@ -6101,6 +6102,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
|||
| `created_at` | string | false | | |
|
||||
| `deleting_at` | string | false | | Deleting at indicates the time at which the workspace will be permanently deleted. A workspace is eligible for deletion if it is dormant (a non-nil dormant_at value) and a value has been specified for time_til_dormant_autodelete on its template. |
|
||||
| `dormant_at` | string | false | | Dormant at being non-nil indicates a workspace that is dormant. A dormant workspace is no longer accessible must be activated. It is subject to deletion if it breaches the duration of the time*til* field on its template. |
|
||||
| `favorite` | boolean | false | | |
|
||||
| `health` | [codersdk.WorkspaceHealth](#codersdkworkspacehealth) | false | | Health shows the health of the workspace and information about what is causing an unhealthy status. |
|
||||
| `id` | string | false | | |
|
||||
| `last_used_at` | string | false | | |
|
||||
|
@ -7184,6 +7186,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
|||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"deleting_at": "2019-08-24T14:15:22Z",
|
||||
"dormant_at": "2019-08-24T14:15:22Z",
|
||||
"favorite": true,
|
||||
"health": {
|
||||
"failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"healthy": false
|
||||
|
|
|
@ -53,6 +53,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
|
|||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"deleting_at": "2019-08-24T14:15:22Z",
|
||||
"dormant_at": "2019-08-24T14:15:22Z",
|
||||
"favorite": true,
|
||||
"health": {
|
||||
"failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"healthy": false
|
||||
|
@ -264,6 +265,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
|
|||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"deleting_at": "2019-08-24T14:15:22Z",
|
||||
"dormant_at": "2019-08-24T14:15:22Z",
|
||||
"favorite": true,
|
||||
"health": {
|
||||
"failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"healthy": false
|
||||
|
@ -478,6 +480,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \
|
|||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"deleting_at": "2019-08-24T14:15:22Z",
|
||||
"dormant_at": "2019-08-24T14:15:22Z",
|
||||
"favorite": true,
|
||||
"health": {
|
||||
"failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"healthy": false
|
||||
|
@ -686,6 +689,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \
|
|||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"deleting_at": "2019-08-24T14:15:22Z",
|
||||
"dormant_at": "2019-08-24T14:15:22Z",
|
||||
"favorite": true,
|
||||
"health": {
|
||||
"failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"healthy": false
|
||||
|
@ -1013,6 +1017,7 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \
|
|||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"deleting_at": "2019-08-24T14:15:22Z",
|
||||
"dormant_at": "2019-08-24T14:15:22Z",
|
||||
"favorite": true,
|
||||
"health": {
|
||||
"failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"healthy": false
|
||||
|
@ -1245,6 +1250,58 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/extend \
|
|||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Favorite workspace by ID.
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/favorite \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`PUT /workspaces/{workspace}/favorite`
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
| ----------- | ---- | ------------ | -------- | ------------ |
|
||||
| `workspace` | path | string(uuid) | true | Workspace ID |
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
| ------ | --------------------------------------------------------------- | ----------- | ------ |
|
||||
| 204 | [No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5) | No Content | |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Unfavorite workspace by ID.
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X DELETE http://coder-server:8080/api/v2/workspaces/{workspace}/favorite \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`DELETE /workspaces/{workspace}/favorite`
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
| ----------- | ---- | ------------ | -------- | ------------ |
|
||||
| `workspace` | path | string(uuid) | true | Workspace ID |
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
| ------ | --------------------------------------------------------------- | ----------- | ------ |
|
||||
| 204 | [No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5) | No Content | |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Resolve workspace autostart by id.
|
||||
|
||||
### Code samples
|
||||
|
|
|
@ -137,6 +137,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
|
|||
"dormant_at": ActionTrack,
|
||||
"deleting_at": ActionTrack,
|
||||
"automatic_updates": ActionTrack,
|
||||
"favorite": ActionTrack,
|
||||
},
|
||||
&database.WorkspaceBuild{}: {
|
||||
"id": ActionIgnore,
|
||||
|
|
|
@ -1490,6 +1490,7 @@ export interface Workspace {
|
|||
readonly health: WorkspaceHealth;
|
||||
readonly automatic_updates: AutomaticUpdates;
|
||||
readonly allow_renames: boolean;
|
||||
readonly favorite: boolean;
|
||||
}
|
||||
|
||||
// From codersdk/workspaceagents.go
|
||||
|
|
|
@ -1020,6 +1020,7 @@ export const MockWorkspace: TypesGen.Workspace = {
|
|||
},
|
||||
automatic_updates: "never",
|
||||
allow_renames: true,
|
||||
favorite: true,
|
||||
};
|
||||
|
||||
export const MockStoppedWorkspace: TypesGen.Workspace = {
|
||||
|
|
Loading…
Reference in New Issue