Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 50f76b9

Browse files
committed
Merge branch 'main' into agent
2 parents 7f4565c + 67613da commit 50f76b9

19 files changed

+356
-44
lines changed

cli/login.go

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func login() *cobra.Command {
6767
if !isTTY(cmd) {
6868
return xerrors.New("the initial user cannot be created in non-interactive mode. use the API")
6969
}
70-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Your Coder deployment hasn't been set up!\n", color.HiBlackString(">"))
70+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Your Coder deployment hasn't been set up!\n", caret)
7171

7272
_, err := prompt(cmd, &promptui.Prompt{
7373
Label: "Would you like to create the first user?",
@@ -147,13 +147,13 @@ func login() *cobra.Command {
147147
return xerrors.Errorf("write server url: %w", err)
148148
}
149149

150-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Welcome to Coder, %s! You're authenticated.\n", color.HiBlackString(">"), color.HiCyanString(username))
150+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Welcome to Coder, %s! You're authenticated.\n", caret, color.HiCyanString(username))
151151
return nil
152152
}
153153

154154
authURL := *serverURL
155155
authURL.Path = serverURL.Path + "/cli-auth"
156-
if err := openURL(authURL.String()); err != nil {
156+
if err := openURL(cmd, authURL.String()); err != nil {
157157
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Open the following in your browser:\n\n\t%s\n\n", authURL.String())
158158
} else {
159159
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Your browser has been opened to visit:\n\n\t%s\n\n", authURL.String())
@@ -192,7 +192,7 @@ func login() *cobra.Command {
192192
return xerrors.Errorf("write server url: %w", err)
193193
}
194194

195-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Welcome to Coder, %s! You're authenticated.\n", color.HiBlackString(">"), color.HiCyanString(resp.Username))
195+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Welcome to Coder, %s! You're authenticated.\n", caret, color.HiCyanString(resp.Username))
196196
return nil
197197
},
198198
}
@@ -211,21 +211,22 @@ func isWSL() (bool, error) {
211211
}
212212

213213
// openURL opens the provided URL via user's default browser
214-
func openURL(urlToOpen string) error {
215-
var cmd string
216-
var args []string
217-
214+
func openURL(cmd *cobra.Command, urlToOpen string) error {
215+
noOpen, err := cmd.Flags().GetBool(varNoOpen)
216+
if err != nil {
217+
panic(err)
218+
}
219+
if noOpen {
220+
return xerrors.New("opening is blocked")
221+
}
218222
wsl, err := isWSL()
219223
if err != nil {
220224
return xerrors.Errorf("test running Windows Subsystem for Linux: %w", err)
221225
}
222226

223227
if wsl {
224-
cmd = "cmd.exe"
225-
args = []string{"/c", "start"}
226-
urlToOpen = strings.ReplaceAll(urlToOpen, "&", "^&")
227-
args = append(args, urlToOpen)
228-
return exec.Command(cmd, args...).Start()
228+
// #nosec
229+
return exec.Command("cmd.exe", "/c", "start", strings.ReplaceAll(urlToOpen, "&", "^&")).Start()
229230
}
230231

231232
return browser.OpenURL(urlToOpen)

cli/login_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func TestLogin(t *testing.T) {
6969
})
7070
require.NoError(t, err)
7171

72-
root, _ := clitest.New(t, "login", client.URL.String(), "--force-tty")
72+
root, _ := clitest.New(t, "login", client.URL.String(), "--force-tty", "--no-open")
7373
pty := ptytest.New(t)
7474
root.SetIn(pty.Input())
7575
root.SetOut(pty.Output())
@@ -94,7 +94,7 @@ func TestLogin(t *testing.T) {
9494
})
9595
require.NoError(t, err)
9696

97-
root, _ := clitest.New(t, "login", client.URL.String(), "--force-tty")
97+
root, _ := clitest.New(t, "login", client.URL.String(), "--force-tty", "--no-open")
9898
pty := ptytest.New(t)
9999
root.SetIn(pty.Input())
100100
root.SetOut(pty.Output())

cli/projectcreate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ func projectCreate() *cobra.Command {
9292
return err
9393
}
9494

95-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s The %s project has been created!\n", color.HiBlackString(">"), color.HiCyanString(project.Name))
95+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s The %s project has been created!\n", caret, color.HiCyanString(project.Name))
9696
_, err = prompt(cmd, &promptui.Prompt{
9797
Label: "Create a new workspace?",
9898
IsConfirm: true,

cli/projectlist.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"text/tabwriter"
6+
"time"
7+
8+
"github.com/fatih/color"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
func projectList() *cobra.Command {
13+
return &cobra.Command{
14+
Use: "list",
15+
Aliases: []string{"ls"},
16+
RunE: func(cmd *cobra.Command, args []string) error {
17+
client, err := createClient(cmd)
18+
if err != nil {
19+
return err
20+
}
21+
start := time.Now()
22+
organization, err := currentOrganization(cmd, client)
23+
if err != nil {
24+
return err
25+
}
26+
projects, err := client.Projects(cmd.Context(), organization.Name)
27+
if err != nil {
28+
return err
29+
}
30+
31+
if len(projects) == 0 {
32+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s No projects found in %s! Create one:\n\n", caret, color.HiWhiteString(organization.Name))
33+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), color.HiMagentaString(" $ coder projects create <directory>\n"))
34+
return nil
35+
}
36+
37+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Projects found in %s %s\n\n",
38+
caret,
39+
color.HiWhiteString(organization.Name),
40+
color.HiBlackString("[%dms]",
41+
time.Since(start).Milliseconds()))
42+
43+
writer := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 4, ' ', 0)
44+
_, _ = fmt.Fprintf(writer, "%s\t%s\t%s\t%s\n",
45+
color.HiBlackString("Project"),
46+
color.HiBlackString("Source"),
47+
color.HiBlackString("Last Updated"),
48+
color.HiBlackString("Used By"))
49+
for _, project := range projects {
50+
suffix := ""
51+
if project.WorkspaceOwnerCount != 1 {
52+
suffix = "s"
53+
}
54+
_, _ = fmt.Fprintf(writer, "%s\t%s\t%s\t%s\n",
55+
color.New(color.FgHiCyan).Sprint(project.Name),
56+
color.WhiteString("Archive"),
57+
color.WhiteString(project.UpdatedAt.Format("January 2, 2006")),
58+
color.New(color.FgHiWhite).Sprintf("%d developer%s", project.WorkspaceOwnerCount, suffix))
59+
}
60+
return writer.Flush()
61+
},
62+
}
63+
}

cli/projectlist_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package cli_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
"github.com/coder/coder/cli/clitest"
9+
"github.com/coder/coder/coderd/coderdtest"
10+
"github.com/coder/coder/pty/ptytest"
11+
)
12+
13+
func TestProjectList(t *testing.T) {
14+
t.Parallel()
15+
t.Run("None", func(t *testing.T) {
16+
t.Parallel()
17+
client := coderdtest.New(t, nil)
18+
coderdtest.CreateInitialUser(t, client)
19+
cmd, root := clitest.New(t, "projects", "list")
20+
clitest.SetupConfig(t, client, root)
21+
pty := ptytest.New(t)
22+
cmd.SetIn(pty.Input())
23+
cmd.SetOut(pty.Output())
24+
closeChan := make(chan struct{})
25+
go func() {
26+
err := cmd.Execute()
27+
require.NoError(t, err)
28+
close(closeChan)
29+
}()
30+
pty.ExpectMatch("No projects found")
31+
<-closeChan
32+
})
33+
t.Run("List", func(t *testing.T) {
34+
t.Parallel()
35+
client := coderdtest.New(t, nil)
36+
user := coderdtest.CreateInitialUser(t, client)
37+
daemon := coderdtest.NewProvisionerDaemon(t, client)
38+
job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil)
39+
coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID)
40+
_ = daemon.Close()
41+
project := coderdtest.CreateProject(t, client, user.Organization, job.ID)
42+
cmd, root := clitest.New(t, "projects", "list")
43+
clitest.SetupConfig(t, client, root)
44+
pty := ptytest.New(t)
45+
cmd.SetIn(pty.Input())
46+
cmd.SetOut(pty.Output())
47+
closeChan := make(chan struct{})
48+
go func() {
49+
err := cmd.Execute()
50+
require.NoError(t, err)
51+
close(closeChan)
52+
}()
53+
pty.ExpectMatch(project.Name)
54+
<-closeChan
55+
})
56+
}

cli/projects.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@ func projects() *cobra.Command {
3030
3131
` + color.New(color.FgHiMagenta).Sprint("$ coder projects update <name>"),
3232
}
33-
cmd.AddCommand(projectCreate())
34-
cmd.AddCommand(projectPlan())
35-
cmd.AddCommand(projectUpdate())
33+
cmd.AddCommand(
34+
projectCreate(),
35+
projectList(),
36+
projectPlan(),
37+
projectUpdate(),
38+
)
3639

3740
return cmd
3841
}

cli/root.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,13 @@ import (
1818
"github.com/coder/coder/codersdk"
1919
)
2020

21+
var (
22+
caret = color.HiBlackString(">")
23+
)
24+
2125
const (
2226
varGlobalConfig = "global-config"
27+
varNoOpen = "no-open"
2328
varForceTty = "force-tty"
2429
)
2530

@@ -71,6 +76,11 @@ func Root() *cobra.Command {
7176
// This should never return an error, because we just added the `--force-tty`` flag prior to calling MarkHidden.
7277
panic(err)
7378
}
79+
cmd.PersistentFlags().Bool(varNoOpen, false, "Block automatically opening URLs in the browser.")
80+
err = cmd.PersistentFlags().MarkHidden(varNoOpen)
81+
if err != nil {
82+
panic(err)
83+
}
7484

7585
return cmd
7686
}

cli/workspacecreate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func workspaceCreate() *cobra.Command {
5454
}
5555
}
5656

57-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Previewing project create...\n", color.HiBlackString(">"))
57+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Previewing project create...\n", caret)
5858

5959
project, err := client.Project(cmd.Context(), organization.Name, args[0])
6060
if err != nil {

coderd/projects.go

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"net/http"
8+
"time"
89

910
"github.com/go-chi/render"
1011
"github.com/google/uuid"
@@ -30,7 +31,16 @@ type CreateParameterValueRequest struct {
3031
// Project is the JSON representation of a Coder project.
3132
// This type matches the database object for now, but is
3233
// abstracted for ease of change later on.
33-
type Project database.Project
34+
type Project struct {
35+
ID uuid.UUID `json:"id"`
36+
CreatedAt time.Time `json:"created_at"`
37+
UpdatedAt time.Time `json:"updated_at"`
38+
OrganizationID string `json:"organization_id"`
39+
Name string `json:"name"`
40+
Provisioner database.ProvisionerType `json:"provisioner"`
41+
ActiveVersionID uuid.UUID `json:"active_version_id"`
42+
WorkspaceOwnerCount uint32 `json:"workspace_owner_count"`
43+
}
3444

3545
// CreateProjectRequest enables callers to create a new Project.
3646
type CreateProjectRequest struct {
@@ -69,11 +79,22 @@ func (api *api) projects(rw http.ResponseWriter, r *http.Request) {
6979
})
7080
return
7181
}
72-
if projects == nil {
73-
projects = []database.Project{}
82+
projectIDs := make([]uuid.UUID, 0, len(projects))
83+
for _, project := range projects {
84+
projectIDs = append(projectIDs, project.ID)
85+
}
86+
workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByProjectIDs(r.Context(), projectIDs)
87+
if errors.Is(err, sql.ErrNoRows) {
88+
err = nil
89+
}
90+
if err != nil {
91+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
92+
Message: fmt.Sprintf("get workspace counts: %s", err.Error()),
93+
})
94+
return
7495
}
7596
render.Status(r, http.StatusOK)
76-
render.JSON(rw, r, projects)
97+
render.JSON(rw, r, convertProjects(projects, workspaceCounts))
7798
}
7899

79100
// Lists all projects in an organization.
@@ -89,11 +110,22 @@ func (api *api) projectsByOrganization(rw http.ResponseWriter, r *http.Request)
89110
})
90111
return
91112
}
92-
if projects == nil {
93-
projects = []database.Project{}
113+
projectIDs := make([]uuid.UUID, 0, len(projects))
114+
for _, project := range projects {
115+
projectIDs = append(projectIDs, project.ID)
116+
}
117+
workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByProjectIDs(r.Context(), projectIDs)
118+
if errors.Is(err, sql.ErrNoRows) {
119+
err = nil
120+
}
121+
if err != nil {
122+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
123+
Message: fmt.Sprintf("get workspace counts: %s", err.Error()),
124+
})
125+
return
94126
}
95127
render.Status(r, http.StatusOK)
96-
render.JSON(rw, r, projects)
128+
render.JSON(rw, r, convertProjects(projects, workspaceCounts))
97129
}
98130

99131
// Create a new project in an organization.
@@ -162,7 +194,7 @@ func (api *api) postProjectsByOrganization(rw http.ResponseWriter, r *http.Reque
162194
if err != nil {
163195
return xerrors.Errorf("insert project version: %s", err)
164196
}
165-
project = Project(dbProject)
197+
project = convertProject(dbProject, 0)
166198
return nil
167199
})
168200
if err != nil {
@@ -241,6 +273,38 @@ func (api *api) parametersByProject(rw http.ResponseWriter, r *http.Request) {
241273
render.JSON(rw, r, apiParameterValues)
242274
}
243275

276+
func convertProjects(projects []database.Project, workspaceCounts []database.GetWorkspaceOwnerCountsByProjectIDsRow) []Project {
277+
apiProjects := make([]Project, 0, len(projects))
278+
for _, project := range projects {
279+
found := false
280+
for _, workspaceCount := range workspaceCounts {
281+
if workspaceCount.ProjectID.String() != project.ID.String() {
282+
continue
283+
}
284+
apiProjects = append(apiProjects, convertProject(project, uint32(workspaceCount.Count)))
285+
found = true
286+
break
287+
}
288+
if !found {
289+
apiProjects = append(apiProjects, convertProject(project, uint32(0)))
290+
}
291+
}
292+
return apiProjects
293+
}
294+
295+
func convertProject(project database.Project, workspaceOwnerCount uint32) Project {
296+
return Project{
297+
ID: project.ID,
298+
CreatedAt: project.CreatedAt,
299+
UpdatedAt: project.UpdatedAt,
300+
OrganizationID: project.OrganizationID,
301+
Name: project.Name,
302+
Provisioner: project.Provisioner,
303+
ActiveVersionID: project.ActiveVersionID,
304+
WorkspaceOwnerCount: workspaceOwnerCount,
305+
}
306+
}
307+
244308
func convertParameterValue(parameterValue database.ParameterValue) ParameterValue {
245309
parameterValue.SourceValue = ""
246310
return ParameterValue(parameterValue)

0 commit comments

Comments
 (0)