Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b7b40f40b9 |
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Mux tool post-hook to format Go files after edits.
|
||||
# It runs after file_edit_* tools and formats only .go files.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${MUX_TOOL:-}" != file_edit_* ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
tool_input="${MUX_TOOL_INPUT:-}"
|
||||
if [[ -n "${MUX_TOOL_INPUT_PATH:-}" && -f "${MUX_TOOL_INPUT_PATH}" ]]; then
|
||||
tool_input="$(cat "${MUX_TOOL_INPUT_PATH}")"
|
||||
fi
|
||||
|
||||
if [[ -z "$tool_input" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
file_path="$(echo "$tool_input" | jq -r '.file_path // empty')"
|
||||
if [[ -z "$file_path" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "$file_path" in
|
||||
*.go)
|
||||
project_dir="${MUX_PROJECT_DIR:-$(pwd)}"
|
||||
cd "$project_dir"
|
||||
if command -v brew >/dev/null 2>&1; then
|
||||
brew_prefix="$(brew --prefix gnu-getopt 2>/dev/null || true)"
|
||||
if [[ -n "$brew_prefix" && -d "$brew_prefix/bin" ]]; then
|
||||
export PATH="$brew_prefix/bin:$PATH"
|
||||
fi
|
||||
fi
|
||||
if ! make fmt/go FILE="$file_path" >/dev/null 2>&1; then
|
||||
if command -v gofmt >/dev/null 2>&1; then
|
||||
gofmt -w "$file_path"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
+24
-25
@@ -39,7 +39,6 @@ import (
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/clistat"
|
||||
"github.com/coder/coder/v2/agent/agentcontainers"
|
||||
"github.com/coder/coder/v2/agent/agentutil"
|
||||
"github.com/coder/coder/v2/agent/agentexec"
|
||||
"github.com/coder/coder/v2/agent/agentfiles"
|
||||
"github.com/coder/coder/v2/agent/agentscripts"
|
||||
@@ -554,7 +553,7 @@ func (a *agent) reportMetadata(ctx context.Context, aAPI proto.DRPCAgentClient28
|
||||
// Set up collect and report as a single ticker with two channels,
|
||||
// this is to allow collection and reporting to be triggered
|
||||
// independently of each other.
|
||||
agentutil.Go(ctx, a.logger, func() {
|
||||
go func() {
|
||||
t := time.NewTicker(a.reportMetadataInterval)
|
||||
defer func() {
|
||||
t.Stop()
|
||||
@@ -579,9 +578,9 @@ func (a *agent) reportMetadata(ctx context.Context, aAPI proto.DRPCAgentClient28
|
||||
wake(collect)
|
||||
}
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
agentutil.Go(ctx, a.logger, func() {
|
||||
go func() {
|
||||
defer close(collectDone)
|
||||
|
||||
var (
|
||||
@@ -628,7 +627,7 @@ func (a *agent) reportMetadata(ctx context.Context, aAPI proto.DRPCAgentClient28
|
||||
// We send the result to the channel in the goroutine to avoid
|
||||
// sending the same result multiple times. So, we don't care about
|
||||
// the return values.
|
||||
agentutil.Go(ctx, a.logger, func() { flight.Do(md.Key, func() {
|
||||
go flight.Do(md.Key, func() {
|
||||
ctx := slog.With(ctx, slog.F("key", md.Key))
|
||||
lastCollectedAtMu.RLock()
|
||||
collectedAt, ok := lastCollectedAts[md.Key]
|
||||
@@ -681,10 +680,10 @@ func (a *agent) reportMetadata(ctx context.Context, aAPI proto.DRPCAgentClient28
|
||||
lastCollectedAts[md.Key] = now
|
||||
lastCollectedAtMu.Unlock()
|
||||
}
|
||||
}) })
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
// Gather metadata updates and report them once every interval. If a
|
||||
// previous report is in flight, wait for it to complete before
|
||||
@@ -735,14 +734,14 @@ func (a *agent) reportMetadata(ctx context.Context, aAPI proto.DRPCAgentClient28
|
||||
}
|
||||
|
||||
reportInFlight = true
|
||||
agentutil.Go(ctx, a.logger, func() {
|
||||
go func() {
|
||||
a.logger.Debug(ctx, "batch updating metadata")
|
||||
ctx, cancel := context.WithTimeout(ctx, reportTimeout)
|
||||
defer cancel()
|
||||
|
||||
_, err := aAPI.BatchUpdateMetadata(ctx, &proto.BatchUpdateMetadataRequest{Metadata: metadata})
|
||||
reportError <- err
|
||||
})
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1519,10 +1518,10 @@ func (a *agent) trackGoroutine(fn func()) error {
|
||||
return xerrors.Errorf("track conn goroutine: %w", ErrAgentClosing)
|
||||
}
|
||||
a.closeWaitGroup.Add(1)
|
||||
agentutil.Go(a.hardCtx, a.logger, func() {
|
||||
go func() {
|
||||
defer a.closeWaitGroup.Done()
|
||||
fn()
|
||||
})
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1626,15 +1625,15 @@ func (a *agent) createTailnet(
|
||||
clog.Info(ctx, "accepted conn")
|
||||
wg.Add(1)
|
||||
closed := make(chan struct{})
|
||||
agentutil.Go(ctx, clog, func() {
|
||||
go func() {
|
||||
select {
|
||||
case <-closed:
|
||||
case <-a.hardCtx.Done():
|
||||
_ = conn.Close()
|
||||
}
|
||||
wg.Done()
|
||||
})
|
||||
agentutil.Go(ctx, clog, func() {
|
||||
}()
|
||||
go func() {
|
||||
defer close(closed)
|
||||
sErr := speedtest.ServeConn(conn)
|
||||
if sErr != nil {
|
||||
@@ -1642,7 +1641,7 @@ func (a *agent) createTailnet(
|
||||
return
|
||||
}
|
||||
clog.Info(ctx, "test ended")
|
||||
})
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}); err != nil {
|
||||
@@ -1669,13 +1668,13 @@ func (a *agent) createTailnet(
|
||||
WriteTimeout: 20 * time.Second,
|
||||
ErrorLog: slog.Stdlib(ctx, a.logger.Named("http_api_server"), slog.LevelInfo),
|
||||
}
|
||||
agentutil.Go(ctx, a.logger, func() {
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-a.hardCtx.Done():
|
||||
}
|
||||
_ = server.Close()
|
||||
})
|
||||
}()
|
||||
|
||||
apiServErr := server.Serve(apiListener)
|
||||
if apiServErr != nil && !xerrors.Is(apiServErr, http.ErrServerClosed) && !strings.Contains(apiServErr.Error(), "use of closed network connection") {
|
||||
@@ -1717,7 +1716,7 @@ func (a *agent) runCoordinator(ctx context.Context, tClient tailnetproto.DRPCTai
|
||||
coordination := ctrl.New(coordinate)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
agentutil.Go(ctx, a.logger, func() {
|
||||
go func() {
|
||||
defer close(errCh)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -1729,7 +1728,7 @@ func (a *agent) runCoordinator(ctx context.Context, tClient tailnetproto.DRPCTai
|
||||
case err := <-coordination.Wait():
|
||||
errCh <- err
|
||||
}
|
||||
})
|
||||
}()
|
||||
return <-errCh
|
||||
}
|
||||
|
||||
@@ -1820,7 +1819,7 @@ func (a *agent) Collect(ctx context.Context, networkStats map[netlogtype.Connect
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
agentutil.Go(pingCtx, a.logger, func() {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
duration, p2p, _, err := a.network.Ping(pingCtx, addresses[0].Addr())
|
||||
if err != nil {
|
||||
@@ -1834,7 +1833,7 @@ func (a *agent) Collect(ctx context.Context, networkStats map[netlogtype.Connect
|
||||
} else {
|
||||
derpConns++
|
||||
}
|
||||
})
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
sort.Float64s(durations)
|
||||
@@ -2032,13 +2031,13 @@ func (a *agent) Close() error {
|
||||
|
||||
// Wait for the graceful shutdown to complete, but don't wait forever so
|
||||
// that we don't break user expectations.
|
||||
agentutil.Go(a.hardCtx, a.logger, func() {
|
||||
go func() {
|
||||
defer a.hardCancel()
|
||||
select {
|
||||
case <-a.hardCtx.Done():
|
||||
case <-time.After(5 * time.Second):
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
// Wait for lifecycle to be reported
|
||||
lifecycleWaitLoop:
|
||||
@@ -2128,13 +2127,13 @@ const EnvAgentSubsystem = "CODER_AGENT_SUBSYSTEM"
|
||||
// eitherContext returns a context that is canceled when either context ends.
|
||||
func eitherContext(a, b context.Context) context.Context {
|
||||
ctx, cancel := context.WithCancel(a)
|
||||
agentutil.Go(ctx, slog.Logger{}, func() {
|
||||
go func() {
|
||||
defer cancel()
|
||||
select {
|
||||
case <-a.Done():
|
||||
case <-b.Done():
|
||||
}
|
||||
})
|
||||
}()
|
||||
return ctx
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ import (
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/agent/agentcontainers/ignore"
|
||||
"github.com/coder/coder/v2/agent/agentutil"
|
||||
"github.com/coder/coder/v2/agent/agentcontainers/watcher"
|
||||
"github.com/coder/coder/v2/agent/agentexec"
|
||||
"github.com/coder/coder/v2/agent/usershell"
|
||||
@@ -564,11 +563,11 @@ func (api *API) discoverDevcontainersInProject(projectPath string) error {
|
||||
|
||||
if dc.Status == codersdk.WorkspaceAgentDevcontainerStatusStarting {
|
||||
api.asyncWg.Add(1)
|
||||
agentutil.Go(api.ctx, api.logger, func() {
|
||||
go func() {
|
||||
defer api.asyncWg.Done()
|
||||
|
||||
_ = api.CreateDevcontainer(dc.WorkspaceFolder, dc.ConfigPath)
|
||||
})
|
||||
}()
|
||||
}
|
||||
}
|
||||
api.mu.Unlock()
|
||||
@@ -1424,9 +1423,9 @@ func (api *API) handleDevcontainerRecreate(w http.ResponseWriter, r *http.Reques
|
||||
api.knownDevcontainers[dc.WorkspaceFolder] = dc
|
||||
api.broadcastUpdatesLocked()
|
||||
|
||||
agentutil.Go(ctx, api.logger, func() {
|
||||
go func() {
|
||||
_ = api.CreateDevcontainer(dc.WorkspaceFolder, dc.ConfigPath, WithRemoveExistingContainer())
|
||||
})
|
||||
}()
|
||||
|
||||
api.mu.Unlock()
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/agent/agentssh"
|
||||
"github.com/coder/coder/v2/agent/agentutil"
|
||||
"github.com/coder/coder/v2/agent/proto"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
@@ -474,10 +473,10 @@ func (r *Runner) trackCommandGoroutine(fn func()) error {
|
||||
return xerrors.New("track command goroutine: closed")
|
||||
}
|
||||
r.cmdCloseWait.Add(1)
|
||||
agentutil.Go(r.cronCtx, r.Logger, func() {
|
||||
go func() {
|
||||
defer r.cmdCloseWait.Done()
|
||||
fn()
|
||||
})
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/agent/agentsocket/proto"
|
||||
"github.com/coder/coder/v2/agent/agentutil"
|
||||
"github.com/coder/coder/v2/agent/unit"
|
||||
"github.com/coder/coder/v2/codersdk/drpcsdk"
|
||||
)
|
||||
@@ -80,10 +79,10 @@ func NewServer(logger slog.Logger, opts ...Option) (*Server, error) {
|
||||
server.logger.Info(server.ctx, "agent socket server started", slog.F("path", server.path))
|
||||
|
||||
server.wg.Add(1)
|
||||
agentutil.Go(server.ctx, server.logger, func() {
|
||||
go func() {
|
||||
defer server.wg.Done()
|
||||
server.acceptConnections()
|
||||
})
|
||||
}()
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
+10
-11
@@ -29,7 +29,6 @@ import (
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/agent/agentcontainers"
|
||||
"github.com/coder/coder/v2/agent/agentutil"
|
||||
"github.com/coder/coder/v2/agent/agentexec"
|
||||
"github.com/coder/coder/v2/agent/agentrsa"
|
||||
"github.com/coder/coder/v2/agent/usershell"
|
||||
@@ -635,13 +634,13 @@ func (s *Server) startNonPTYSession(logger slog.Logger, session ssh.Session, mag
|
||||
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "no", "stdin_pipe").Add(1)
|
||||
return xerrors.Errorf("create stdin pipe: %w", err)
|
||||
}
|
||||
agentutil.Go(session.Context(), logger, func() {
|
||||
go func() {
|
||||
_, err := io.Copy(stdinPipe, session)
|
||||
if err != nil {
|
||||
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "no", "stdin_io_copy").Add(1)
|
||||
}
|
||||
_ = stdinPipe.Close()
|
||||
})
|
||||
}()
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "no", "start_command").Add(1)
|
||||
@@ -663,11 +662,11 @@ func (s *Server) startNonPTYSession(logger slog.Logger, session ssh.Session, mag
|
||||
session.Signals(nil)
|
||||
close(sigs)
|
||||
}()
|
||||
agentutil.Go(session.Context(), logger, func() {
|
||||
go func() {
|
||||
for sig := range sigs {
|
||||
handleSignal(logger, sig, cmd.Process, s.metrics, magicTypeLabel)
|
||||
}
|
||||
})
|
||||
}()
|
||||
return cmd.Wait()
|
||||
}
|
||||
|
||||
@@ -738,7 +737,7 @@ func (s *Server) startPTYSession(logger slog.Logger, session ptySession, magicTy
|
||||
session.Signals(nil)
|
||||
close(sigs)
|
||||
}()
|
||||
agentutil.Go(ctx, logger, func() {
|
||||
go func() {
|
||||
for {
|
||||
if sigs == nil && windowSize == nil {
|
||||
return
|
||||
@@ -765,14 +764,14 @@ func (s *Server) startPTYSession(logger slog.Logger, session ptySession, magicTy
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
agentutil.Go(ctx, logger, func() {
|
||||
go func() {
|
||||
_, err := io.Copy(ptty.InputWriter(), session)
|
||||
if err != nil {
|
||||
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "input_io_copy").Add(1)
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
// We need to wait for the command output to finish copying. It's safe to
|
||||
// just do this copy on the main handler goroutine because one of two things
|
||||
@@ -1214,11 +1213,11 @@ func (s *Server) Close() error {
|
||||
// but Close() may not have completed.
|
||||
func (s *Server) Shutdown(ctx context.Context) error {
|
||||
ch := make(chan error, 1)
|
||||
agentutil.Go(ctx, s.logger, func() {
|
||||
go func() {
|
||||
// TODO(mafredri): Implement shutdown, SIGHUP running commands, etc.
|
||||
// For now we just close the server.
|
||||
ch <- s.Close()
|
||||
})
|
||||
}()
|
||||
var err error
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
||||
@@ -4,9 +4,6 @@ import (
|
||||
"context"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/agent/agentutil"
|
||||
)
|
||||
|
||||
// Bicopy copies all of the data between the two connections and will close them
|
||||
@@ -38,10 +35,10 @@ func Bicopy(ctx context.Context, c1, c2 io.ReadWriteCloser) {
|
||||
|
||||
// Convert waitgroup to a channel so we can also wait on the context.
|
||||
done := make(chan struct{})
|
||||
agentutil.Go(ctx, slog.Logger{}, func() {
|
||||
go func() {
|
||||
defer close(done)
|
||||
wg.Wait()
|
||||
})
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/agent/agentutil"
|
||||
)
|
||||
|
||||
// streamLocalForwardPayload describes the extra data sent in a
|
||||
@@ -131,11 +130,11 @@ func (h *forwardedUnixHandler) HandleSSHRequest(ctx ssh.Context, _ *ssh.Server,
|
||||
log.Debug(ctx, "SSH unix forward added to cache")
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
agentutil.Go(ctx, log, func() {
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
_ = ln.Close()
|
||||
})
|
||||
agentutil.Go(ctx, log, func() {
|
||||
}()
|
||||
go func() {
|
||||
defer cancel()
|
||||
|
||||
for {
|
||||
@@ -153,7 +152,7 @@ func (h *forwardedUnixHandler) HandleSSHRequest(ctx ssh.Context, _ *ssh.Server,
|
||||
SocketPath: addr,
|
||||
})
|
||||
|
||||
agentutil.Go(ctx, log, func() {
|
||||
go func() {
|
||||
ch, reqs, err := conn.OpenChannel("forwarded-streamlocal@openssh.com", payload)
|
||||
if err != nil {
|
||||
h.log.Warn(ctx, "open SSH unix forward channel to client", slog.Error(err))
|
||||
@@ -162,7 +161,7 @@ func (h *forwardedUnixHandler) HandleSSHRequest(ctx ssh.Context, _ *ssh.Server,
|
||||
}
|
||||
go gossh.DiscardRequests(reqs)
|
||||
Bicopy(ctx, ch, c)
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
h.Lock()
|
||||
@@ -172,7 +171,7 @@ func (h *forwardedUnixHandler) HandleSSHRequest(ctx ssh.Context, _ *ssh.Server,
|
||||
h.Unlock()
|
||||
log.Debug(ctx, "SSH unix forward listener removed from cache")
|
||||
_ = ln.Close()
|
||||
})
|
||||
}()
|
||||
|
||||
return true, nil
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/agent/agentutil"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -123,10 +122,10 @@ func (x *x11Forwarder) x11Handler(sshCtx ssh.Context, sshSession ssh.Session) (d
|
||||
}
|
||||
|
||||
// clean up the X11 session if the SSH session completes.
|
||||
agentutil.Go(ctx, x.logger, func() {
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
x.closeAndRemoveSession(x11session)
|
||||
})
|
||||
}()
|
||||
|
||||
go x.listenForConnections(ctx, x11session, serverConn, x11)
|
||||
x.logger.Debug(ctx, "X11 forwarding started", slog.F("display", x11session.display))
|
||||
@@ -207,10 +206,10 @@ func (x *x11Forwarder) listenForConnections(
|
||||
_ = conn.Close()
|
||||
continue
|
||||
}
|
||||
agentutil.Go(ctx, x.logger, func() {
|
||||
go func() {
|
||||
defer x.trackConn(conn, false)
|
||||
Bicopy(ctx, conn, channel)
|
||||
})
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package agentutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime/debug"
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
)
|
||||
|
||||
// Go runs the provided function in a goroutine, recovering from panics and
|
||||
// logging them before re-panicking.
|
||||
func Go(ctx context.Context, log slog.Logger, fn func()) {
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Critical(ctx, "panic in goroutine",
|
||||
slog.F("panic", r),
|
||||
slog.F("stack", string(debug.Stack())),
|
||||
)
|
||||
panic(r)
|
||||
}
|
||||
}()
|
||||
fn()
|
||||
}()
|
||||
}
|
||||
+2
-3
@@ -10,7 +10,6 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/agent/agentutil"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
"github.com/coder/quartz"
|
||||
@@ -70,7 +69,7 @@ func NewAppHealthReporterWithClock(
|
||||
continue
|
||||
}
|
||||
app := nextApp
|
||||
agentutil.Go(ctx, logger, func() {
|
||||
go func() {
|
||||
_ = clk.TickerFunc(ctx, time.Duration(app.Healthcheck.Interval)*time.Second, func() error {
|
||||
// We time out at the healthcheck interval to prevent getting too backed up, but
|
||||
// set it 1ms early so that it's not simultaneous with the next tick in testing,
|
||||
@@ -134,7 +133,7 @@ func NewAppHealthReporterWithClock(
|
||||
}
|
||||
return nil
|
||||
}, "healthcheck", app.Slug)
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/agent/agentutil"
|
||||
"github.com/coder/coder/v2/agent/boundarylogproxy/codec"
|
||||
agentproto "github.com/coder/coder/v2/agent/proto"
|
||||
)
|
||||
@@ -134,11 +133,11 @@ func (s *Server) handleConnection(ctx context.Context, conn net.Conn) {
|
||||
defer cancel()
|
||||
|
||||
s.wg.Add(1)
|
||||
agentutil.Go(ctx, s.logger, func() {
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
<-ctx.Done()
|
||||
_ = conn.Close()
|
||||
})
|
||||
}()
|
||||
|
||||
// This is intended to be a sane starting point for the read buffer size. It may be
|
||||
// grown by codec.ReadFrame if necessary.
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/agent/agentexec"
|
||||
"github.com/coder/coder/v2/agent/agentutil"
|
||||
"github.com/coder/coder/v2/pty"
|
||||
)
|
||||
|
||||
@@ -77,7 +76,7 @@ func newBuffered(ctx context.Context, logger slog.Logger, execer agentexec.Exece
|
||||
// We do not need to separately monitor for the process exiting. When it
|
||||
// exits, our ptty.OutputReader() will return EOF after reading all process
|
||||
// output.
|
||||
agentutil.Go(ctx, logger, func() {
|
||||
go func() {
|
||||
buffer := make([]byte, 1024)
|
||||
for {
|
||||
read, err := ptty.OutputReader().Read(buffer)
|
||||
@@ -119,7 +118,7 @@ func newBuffered(ctx context.Context, logger slog.Logger, execer agentexec.Exece
|
||||
}
|
||||
rpty.state.cond.L.Unlock()
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
return rpty
|
||||
}
|
||||
@@ -134,7 +133,7 @@ func (rpty *bufferedReconnectingPTY) lifecycle(ctx context.Context, logger slog.
|
||||
logger.Debug(ctx, "reconnecting pty ready")
|
||||
rpty.state.setState(StateReady, nil)
|
||||
|
||||
state, reasonErr := rpty.state.waitForStateOrContext(ctx, StateClosing, logger)
|
||||
state, reasonErr := rpty.state.waitForStateOrContext(ctx, StateClosing)
|
||||
if state < StateClosing {
|
||||
// If we have not closed yet then the context is what unblocked us (which
|
||||
// means the agent is shutting down) so move into the closing phase.
|
||||
@@ -191,7 +190,7 @@ func (rpty *bufferedReconnectingPTY) Attach(ctx context.Context, connID string,
|
||||
delete(rpty.activeConns, connID)
|
||||
}()
|
||||
|
||||
state, err := rpty.state.waitForStateOrContext(ctx, StateReady, logger)
|
||||
state, err := rpty.state.waitForStateOrContext(ctx, StateReady)
|
||||
if state != StateReady {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/agent/agentexec"
|
||||
"github.com/coder/coder/v2/agent/agentutil"
|
||||
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
||||
"github.com/coder/coder/v2/pty"
|
||||
)
|
||||
@@ -178,20 +177,20 @@ func (s *ptyState) waitForState(state State) (State, error) {
|
||||
|
||||
// waitForStateOrContext blocks until the state or a greater one is reached or
|
||||
// the provided context ends.
|
||||
func (s *ptyState) waitForStateOrContext(ctx context.Context, state State, logger slog.Logger) (State, error) {
|
||||
func (s *ptyState) waitForStateOrContext(ctx context.Context, state State) (State, error) {
|
||||
s.cond.L.Lock()
|
||||
defer s.cond.L.Unlock()
|
||||
|
||||
nevermind := make(chan struct{})
|
||||
defer close(nevermind)
|
||||
agentutil.Go(ctx, logger, func() {
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Wake up when the context ends.
|
||||
s.cond.Broadcast()
|
||||
case <-nevermind:
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
for ctx.Err() == nil && state > s.state {
|
||||
s.cond.Wait()
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/agent/agentexec"
|
||||
"github.com/coder/coder/v2/agent/agentutil"
|
||||
"github.com/coder/coder/v2/pty"
|
||||
)
|
||||
|
||||
@@ -142,7 +141,7 @@ func (rpty *screenReconnectingPTY) lifecycle(ctx context.Context, logger slog.Lo
|
||||
logger.Debug(ctx, "reconnecting pty ready")
|
||||
rpty.state.setState(StateReady, nil)
|
||||
|
||||
state, reasonErr := rpty.state.waitForStateOrContext(ctx, StateClosing, logger)
|
||||
state, reasonErr := rpty.state.waitForStateOrContext(ctx, StateClosing)
|
||||
if state < StateClosing {
|
||||
// If we have not closed yet then the context is what unblocked us (which
|
||||
// means the agent is shutting down) so move into the closing phase.
|
||||
@@ -167,7 +166,7 @@ func (rpty *screenReconnectingPTY) Attach(ctx context.Context, _ string, conn ne
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
state, err := rpty.state.waitForStateOrContext(ctx, StateReady, logger)
|
||||
state, err := rpty.state.waitForStateOrContext(ctx, StateReady)
|
||||
if state != StateReady {
|
||||
return err
|
||||
}
|
||||
@@ -257,7 +256,7 @@ func (rpty *screenReconnectingPTY) doAttach(ctx context.Context, conn net.Conn,
|
||||
// We do not need to separately monitor for the process exiting. When it
|
||||
// exits, our ptty.OutputReader() will return EOF after reading all process
|
||||
// output.
|
||||
agentutil.Go(ctx, logger, func() {
|
||||
go func() {
|
||||
defer versionCancel()
|
||||
defer func() {
|
||||
err := conn.Close()
|
||||
@@ -299,7 +298,7 @@ func (rpty *screenReconnectingPTY) doAttach(ctx context.Context, conn net.Conn,
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
// Version seems to be the only command without a side effect (other than
|
||||
// making the version pop up briefly) so use it to wait for the session to
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/agent/agentcontainers"
|
||||
"github.com/coder/coder/v2/agent/agentutil"
|
||||
"github.com/coder/coder/v2/agent/agentssh"
|
||||
"github.com/coder/coder/v2/agent/usershell"
|
||||
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
||||
@@ -91,7 +90,7 @@ func (s *Server) Serve(ctx, hardCtx context.Context, l net.Listener) (retErr err
|
||||
wg.Add(1)
|
||||
disconnected := s.reportConnection(uuid.New(), remoteAddrString)
|
||||
closed := make(chan struct{})
|
||||
agentutil.Go(ctx, clog, func() {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
select {
|
||||
case <-closed:
|
||||
@@ -99,9 +98,9 @@ func (s *Server) Serve(ctx, hardCtx context.Context, l net.Listener) (retErr err
|
||||
disconnected(1, "server shut down")
|
||||
_ = conn.Close()
|
||||
}
|
||||
})
|
||||
}()
|
||||
wg.Add(1)
|
||||
agentutil.Go(ctx, clog, func() {
|
||||
go func() {
|
||||
defer close(closed)
|
||||
defer wg.Done()
|
||||
err := s.handleConn(ctx, clog, conn)
|
||||
@@ -114,7 +113,7 @@ func (s *Server) Serve(ctx, hardCtx context.Context, l net.Listener) (retErr err
|
||||
} else {
|
||||
disconnected(0, "")
|
||||
}
|
||||
})
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
return retErr
|
||||
@@ -227,18 +226,18 @@ func (s *Server) handleConn(ctx context.Context, logger slog.Logger, conn net.Co
|
||||
)
|
||||
|
||||
done := make(chan struct{})
|
||||
agentutil.Go(ctx, connLogger, func() {
|
||||
go func() {
|
||||
select {
|
||||
case <-done:
|
||||
case <-ctx.Done():
|
||||
rpty.Close(ctx.Err())
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
agentutil.Go(ctx, connLogger, func() {
|
||||
go func() {
|
||||
rpty.Wait()
|
||||
s.reconnectingPTYs.Delete(msg.ID)
|
||||
})
|
||||
}()
|
||||
|
||||
connected = true
|
||||
sendConnected <- rpty
|
||||
|
||||
+2
-3
@@ -10,7 +10,6 @@ import (
|
||||
"tailscale.com/types/netlogtype"
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/agent/agentutil"
|
||||
"github.com/coder/coder/v2/agent/proto"
|
||||
)
|
||||
|
||||
@@ -87,13 +86,13 @@ func (s *statsReporter) reportLoop(ctx context.Context, dest statsDest) error {
|
||||
// use a separate goroutine to monitor the context so that we notice immediately, rather than
|
||||
// waiting for the next callback (which might never come if we are closing!)
|
||||
ctxDone := false
|
||||
agentutil.Go(ctx, s.logger, func() {
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
s.L.Lock()
|
||||
defer s.L.Unlock()
|
||||
ctxDone = true
|
||||
s.Broadcast()
|
||||
})
|
||||
}()
|
||||
defer s.logger.Debug(ctx, "reportLoop exiting")
|
||||
|
||||
s.L.Lock()
|
||||
|
||||
@@ -470,7 +470,6 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
cdr.dev/slog v1.6.2-0.20251120224544-40ff19937ff2
|
||||
github.com/anthropics/anthropic-sdk-go v1.19.0
|
||||
github.com/brianvoe/gofakeit/v7 v7.14.0
|
||||
github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
cdr.dev/slog v1.6.2-0.20251120224544-40ff19937ff2 h1:M4Z9eTbnHPdZI4GpBUNCae0lSgUucY+aW5j7+zB8lCk=
|
||||
cdr.dev/slog v1.6.2-0.20251120224544-40ff19937ff2/go.mod h1:NaoTA7KwopCrnaSb0JXTC0PTp/O/Y83Lndnq0OEV3ZQ=
|
||||
cdr.dev/slog/v3 v3.0.0-rc1 h1:EN7Zim6GvTpAeHQjI0ERDEfqKbTyXRvgH4UhlzLpvWM=
|
||||
cdr.dev/slog/v3 v3.0.0-rc1/go.mod h1:iO/OALX1VxlI03mkodCGdVP7pXzd2bRMvu3ePvlJ9ak=
|
||||
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
|
||||
|
||||
@@ -104,17 +104,6 @@ func testingWithOwnerUser(m dsl.Matcher) {
|
||||
Report(`This client is operating as the owner user, which has unrestricted permissions. Consider creating a different user.`)
|
||||
}
|
||||
|
||||
// doNotUseRawGoInAgent detects raw `go func()` in agent package.
|
||||
// Use agentutil.Go() instead for panic recovery.
|
||||
//
|
||||
//nolint:unused,deadcode,varnamelen
|
||||
func doNotUseRawGoInAgent(m dsl.Matcher) {
|
||||
m.Match(`go func() { $*_ }()`, `go func($*_) { $*_ }($*_)`).
|
||||
Where(m.File().PkgPath.Matches(`github\.com/coder/coder/v2/agent(/.*)?`) &&
|
||||
!m.File().Name.Matches(`_test\.go$`)).
|
||||
Report("Use agentutil.Go() instead of raw go func() for panic recovery")
|
||||
}
|
||||
|
||||
// Use xerrors everywhere! It provides additional stacktrace info!
|
||||
//
|
||||
//nolint:unused,deadcode,varnamelen
|
||||
|
||||
@@ -18,6 +18,8 @@ export const Popover = PopoverPrimitive.Root;
|
||||
|
||||
export const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
export const PopoverClose = PopoverPrimitive.PopoverClose;
|
||||
|
||||
export const PopoverContent = forwardRef<
|
||||
ElementRef<typeof PopoverPrimitive.Content>,
|
||||
ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
|
||||
@@ -23,7 +23,7 @@ export const DashboardLayout: FC = () => {
|
||||
{canViewDeployment && <LicenseBanner />}
|
||||
<AnnouncementBanners />
|
||||
|
||||
<div className="flex flex-col min-h-screen justify-between">
|
||||
<div className="flex flex-col h-screen justify-between">
|
||||
<Navbar />
|
||||
|
||||
<div className="relative flex flex-col flex-1 min-h-0 overflow-y-auto">
|
||||
|
||||
@@ -61,7 +61,7 @@ export const NavbarView: FC<NavbarViewProps> = ({
|
||||
const webPush = useWebpushNotifications();
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 bg-surface-primary z-40 border-0 border-b border-solid h-[72px] min-h-[72px] flex items-center leading-none px-6">
|
||||
<div className="border-0 border-b border-solid h-[72px] min-h-[72px] flex items-center leading-none px-6">
|
||||
<NavLink to="/workspaces">
|
||||
{logo_url ? (
|
||||
<ExternalImage className="h-7" src={logo_url} alt="Custom Logo" />
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "components/DropdownMenu/DropdownMenu";
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "components/Popover/Popover";
|
||||
import type { FC } from "react";
|
||||
import { UserDropdownContent } from "./UserDropdownContent";
|
||||
|
||||
@@ -22,24 +22,28 @@ export const UserDropdown: FC<UserDropdownProps> = ({
|
||||
onSignOut,
|
||||
}) => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-transparent border-0 cursor-pointer p-0"
|
||||
>
|
||||
<Avatar fallback={user.username} src={user.avatar_url} size="lg" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
</PopoverTrigger>
|
||||
|
||||
<DropdownMenuContent align="end" className="min-w-auto w-[260px]">
|
||||
<PopoverContent
|
||||
align="end"
|
||||
className="min-w-auto w-[260px] bg-surface-secondary border-surface-quaternary"
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<UserDropdownContent
|
||||
user={user}
|
||||
buildInfo={buildInfo}
|
||||
supportLinks={supportLinks}
|
||||
onSignOut={onSignOut}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,31 +1,20 @@
|
||||
import { MockUserOwner } from "testHelpers/entities";
|
||||
import { render, waitForLoaderToBeRemoved } from "testHelpers/renderHelpers";
|
||||
import { screen } from "@testing-library/react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "components/DropdownMenu/DropdownMenu";
|
||||
import { Popover } from "components/Popover/Popover";
|
||||
import { Language, UserDropdownContent } from "./UserDropdownContent";
|
||||
|
||||
const renderUserDropdownContent = (props: { onSignOut: () => void }) => {
|
||||
return render(
|
||||
<DropdownMenu defaultOpen>
|
||||
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<UserDropdownContent
|
||||
user={MockUserOwner}
|
||||
onSignOut={props.onSignOut}
|
||||
supportLinks={[]}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>,
|
||||
);
|
||||
};
|
||||
|
||||
describe("UserDropdownContent", () => {
|
||||
it("has the correct link for the account item", async () => {
|
||||
renderUserDropdownContent({ onSignOut: vi.fn() });
|
||||
render(
|
||||
<Popover>
|
||||
<UserDropdownContent
|
||||
user={MockUserOwner}
|
||||
onSignOut={vi.fn()}
|
||||
supportLinks={[]}
|
||||
/>
|
||||
</Popover>,
|
||||
);
|
||||
await waitForLoaderToBeRemoved();
|
||||
|
||||
const link = screen.getByText(Language.accountLabel).closest("a");
|
||||
@@ -38,7 +27,15 @@ describe("UserDropdownContent", () => {
|
||||
|
||||
it("calls the onSignOut function", async () => {
|
||||
const onSignOut = vi.fn();
|
||||
renderUserDropdownContent({ onSignOut });
|
||||
render(
|
||||
<Popover>
|
||||
<UserDropdownContent
|
||||
user={MockUserOwner}
|
||||
onSignOut={onSignOut}
|
||||
supportLinks={[]}
|
||||
/>
|
||||
</Popover>,
|
||||
);
|
||||
await waitForLoaderToBeRemoved();
|
||||
screen.getByText(Language.signOutLabel).click();
|
||||
expect(onSignOut).toBeCalledTimes(1);
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
} from "components/DropdownMenu/DropdownMenu";
|
||||
type CSSObject,
|
||||
css,
|
||||
type Interpolation,
|
||||
type Theme,
|
||||
} from "@emotion/react";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import { CopyButton } from "components/CopyButton/CopyButton";
|
||||
import { PopoverClose } from "components/Popover/Popover";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import {
|
||||
CheckIcon,
|
||||
CircleUserIcon,
|
||||
CopyIcon,
|
||||
LogOutIcon,
|
||||
MonitorDownIcon,
|
||||
SquareArrowOutUpRightIcon,
|
||||
@@ -40,94 +44,153 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
|
||||
supportLinks,
|
||||
onSignOut,
|
||||
}) => {
|
||||
const { showCopiedSuccess, copyToClipboard } = useClipboard();
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-3 [&_img]:w-full [&_img]:h-full"
|
||||
asChild
|
||||
>
|
||||
<Link to="/settings/account">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-white">{user.username}</span>
|
||||
<span className="text-xs font-semibold">{user.email}</span>
|
||||
</div>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/install">
|
||||
<MonitorDownIcon />
|
||||
<span>Install CLI</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/settings/account">
|
||||
<CircleUserIcon />
|
||||
<span>Account</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={onSignOut}>
|
||||
<LogOutIcon />
|
||||
<span>Sign Out</span>
|
||||
</DropdownMenuItem>
|
||||
{supportLinks && supportLinks.length > 0 && (
|
||||
<div>
|
||||
<Stack css={styles.info} spacing={0}>
|
||||
<span css={styles.userName}>{user.username}</span>
|
||||
<span css={styles.userEmail}>{user.email}</span>
|
||||
</Stack>
|
||||
|
||||
<Divider css={{ marginBottom: 8 }} />
|
||||
|
||||
<Link to="/install" css={styles.link}>
|
||||
<PopoverClose asChild>
|
||||
<MenuItem css={styles.menuItem}>
|
||||
<MonitorDownIcon className="size-5 text-content-secondary" />
|
||||
<span css={styles.menuItemText}>Install CLI</span>
|
||||
</MenuItem>
|
||||
</PopoverClose>
|
||||
</Link>
|
||||
|
||||
<Link to="/settings/account" css={styles.link}>
|
||||
<PopoverClose asChild>
|
||||
<MenuItem css={styles.menuItem}>
|
||||
<CircleUserIcon className="size-5 text-content-secondary" />
|
||||
<span css={styles.menuItemText}>{Language.accountLabel}</span>
|
||||
</MenuItem>
|
||||
</PopoverClose>
|
||||
</Link>
|
||||
|
||||
<MenuItem css={styles.menuItem} onClick={onSignOut}>
|
||||
<LogOutIcon className="size-5 text-content-secondary" />
|
||||
<span css={styles.menuItemText}>{Language.signOutLabel}</span>
|
||||
</MenuItem>
|
||||
|
||||
{supportLinks && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<Divider />
|
||||
{supportLinks.map((link) => (
|
||||
<DropdownMenuItem key={link.name} asChild>
|
||||
<a href={link.target} target="_blank" rel="noreferrer">
|
||||
{link.icon && <SupportIcon icon={link.icon} />}
|
||||
<span>{link.name}</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<a
|
||||
href={link.target}
|
||||
key={link.name}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
css={styles.link}
|
||||
>
|
||||
<PopoverClose asChild>
|
||||
<MenuItem css={styles.menuItem}>
|
||||
{link.icon && (
|
||||
<SupportIcon
|
||||
icon={link.icon}
|
||||
className="size-5 text-content-secondary"
|
||||
/>
|
||||
)}
|
||||
<span css={styles.menuItemText}>{link.name}</span>
|
||||
</MenuItem>
|
||||
</PopoverClose>
|
||||
</a>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<Tooltip disableHoverableContent>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuItem className="text-xs" asChild>
|
||||
|
||||
<Divider css={{ marginBottom: "0 !important" }} />
|
||||
|
||||
<Stack css={styles.info} spacing={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<a
|
||||
css={[styles.footerText, styles.buildInfo]}
|
||||
href={buildInfo?.external_url}
|
||||
className="flex items-center gap-2"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<span className="flex-1">{buildInfo?.version}</span>
|
||||
<SquareArrowOutUpRightIcon className="!size-icon-xs" />
|
||||
{buildInfo?.version} <SquareArrowOutUpRightIcon />
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Browse the source code</TooltipContent>
|
||||
</Tooltip>
|
||||
{buildInfo?.deployment_id && (
|
||||
<Tooltip disableHoverableContent>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="text-xs"
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
copyToClipboard(buildInfo.deployment_id);
|
||||
}}
|
||||
>
|
||||
<span className="truncate flex-1">{buildInfo.deployment_id}</span>
|
||||
{showCopiedSuccess ? (
|
||||
<CheckIcon className="!size-icon-xs ml-auto" />
|
||||
) : (
|
||||
<CopyIcon className="!size-icon-xs ml-auto" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{showCopiedSuccess ? "Copied!" : "Copy deployment ID"}
|
||||
</TooltipContent>
|
||||
<TooltipContent side="bottom">Browse the source code</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<DropdownMenuItem className="text-xs" disabled>
|
||||
<span>{Language.copyrightText}</span>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
|
||||
{buildInfo?.deployment_id && (
|
||||
<div className="flex items-center text-xs">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{buildInfo.deployment_id}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
Deployment Identifier
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<CopyButton
|
||||
text={buildInfo.deployment_id}
|
||||
label="Copy deployment ID"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div css={styles.footerText}>{Language.copyrightText}</div>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
info: (theme) => [
|
||||
theme.typography.body2 as CSSObject,
|
||||
{
|
||||
padding: 20,
|
||||
},
|
||||
],
|
||||
userName: {
|
||||
fontWeight: 600,
|
||||
},
|
||||
userEmail: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
width: "100%",
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
}),
|
||||
link: {
|
||||
textDecoration: "none",
|
||||
color: "inherit",
|
||||
},
|
||||
menuItem: (theme) => css`
|
||||
gap: 20px;
|
||||
padding: 8px 20px;
|
||||
|
||||
&:hover {
|
||||
background-color: ${theme.palette.action.hover};
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
`,
|
||||
menuItemText: {
|
||||
fontSize: 14,
|
||||
},
|
||||
footerText: (theme) => css`
|
||||
font-size: 12px;
|
||||
text-decoration: none;
|
||||
color: ${theme.palette.text.secondary};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
& svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
`,
|
||||
buildInfo: (theme) => ({
|
||||
color: theme.palette.text.primary,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -130,11 +130,19 @@ const TemplateRow: FC<TemplateRowProps> = ({
|
||||
|
||||
<TableCell css={styles.secondary}>
|
||||
{showOrganizations ? (
|
||||
<AvatarData
|
||||
title={template.organization_display_name}
|
||||
subtitle={`Used by ${Language.developerCount(template.active_user_count)}`}
|
||||
avatar={<Avatar variant="icon" src={template.organization_icon} />}
|
||||
/>
|
||||
<Stack
|
||||
spacing={0}
|
||||
css={{
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<span css={styles.cellPrimaryLine}>
|
||||
{template.organization_display_name}
|
||||
</span>
|
||||
<span css={styles.cellSecondaryLine}>
|
||||
Used by {Language.developerCount(template.active_user_count)}
|
||||
</span>
|
||||
</Stack>
|
||||
) : (
|
||||
Language.developerCount(template.active_user_count)
|
||||
)}
|
||||
|
||||
+5
-4
@@ -7,7 +7,6 @@ import {
|
||||
Navigate,
|
||||
Outlet,
|
||||
Route,
|
||||
ScrollRestoration,
|
||||
} from "react-router";
|
||||
import { Loader } from "./components/Loader/Loader";
|
||||
import { RequireAuth } from "./contexts/auth/RequireAuth";
|
||||
@@ -352,11 +351,10 @@ const AIBridgeRequestLogsPage = lazy(
|
||||
() => import("./pages/AIBridgePage/RequestLogsPage/RequestLogsPage"),
|
||||
);
|
||||
|
||||
const GlobalLayout = () => {
|
||||
const RoutesWithSuspense = () => {
|
||||
return (
|
||||
<Suspense fallback={<Loader fullscreen />}>
|
||||
<Outlet />
|
||||
<ScrollRestoration />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
@@ -411,7 +409,10 @@ const groupsRouter = () => {
|
||||
|
||||
export const router = createBrowserRouter(
|
||||
createRoutesFromChildren(
|
||||
<Route element={<GlobalLayout />} errorElement={<GlobalErrorBoundary />}>
|
||||
<Route
|
||||
element={<RoutesWithSuspense />}
|
||||
errorElement={<GlobalErrorBoundary />}
|
||||
>
|
||||
<Route path="login" element={<LoginPage />} />
|
||||
<Route path="login/device" element={<LoginOAuthDevicePage />} />
|
||||
<Route path="setup" element={<SetupPage />} />
|
||||
|
||||
Reference in New Issue
Block a user