diff --git a/agent/agent.go b/agent/agent.go index 8125bbc5f70d6..e3bbe7f07c984 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -155,35 +155,35 @@ func New(options Options) Agent { hardCtx, hardCancel := context.WithCancel(context.Background()) gracefulCtx, gracefulCancel := context.WithCancel(hardCtx) a := &agent{ - tailnetListenPort: options.TailnetListenPort, - reconnectingPTYTimeout: options.ReconnectingPTYTimeout, - logger: options.Logger, - gracefulCtx: gracefulCtx, - gracefulCancel: gracefulCancel, - hardCtx: hardCtx, - hardCancel: hardCancel, - coordDisconnected: make(chan struct{}), - environmentVariables: options.EnvironmentVariables, - client: options.Client, - exchangeToken: options.ExchangeToken, - filesystem: options.Filesystem, - logDir: options.LogDir, - tempDir: options.TempDir, - scriptDataDir: options.ScriptDataDir, - lifecycleUpdate: make(chan struct{}, 1), - lifecycleReported: make(chan codersdk.WorkspaceAgentLifecycle, 1), - lifecycleStates: []agentsdk.PostLifecycleRequest{{State: codersdk.WorkspaceAgentLifecycleCreated}}, - ignorePorts: options.IgnorePorts, - portCacheDuration: options.PortCacheDuration, - reportMetadataInterval: options.ReportMetadataInterval, - serviceBannerRefreshInterval: options.ServiceBannerRefreshInterval, - sshMaxTimeout: options.SSHMaxTimeout, - subsystems: options.Subsystems, - addresses: options.Addresses, - syscaller: options.Syscaller, - modifiedProcs: options.ModifiedProcesses, - processManagementTick: options.ProcessManagementTick, - logSender: agentsdk.NewLogSender(options.Logger), + tailnetListenPort: options.TailnetListenPort, + reconnectingPTYTimeout: options.ReconnectingPTYTimeout, + logger: options.Logger, + gracefulCtx: gracefulCtx, + gracefulCancel: gracefulCancel, + hardCtx: hardCtx, + hardCancel: hardCancel, + coordDisconnected: make(chan struct{}), + environmentVariables: options.EnvironmentVariables, + client: options.Client, + exchangeToken: options.ExchangeToken, + filesystem: options.Filesystem, + logDir: options.LogDir, + tempDir: options.TempDir, + scriptDataDir: options.ScriptDataDir, + lifecycleUpdate: make(chan struct{}, 1), + lifecycleReported: make(chan codersdk.WorkspaceAgentLifecycle, 1), + lifecycleStates: []agentsdk.PostLifecycleRequest{{State: codersdk.WorkspaceAgentLifecycleCreated}}, + ignorePorts: options.IgnorePorts, + portCacheDuration: options.PortCacheDuration, + reportMetadataInterval: options.ReportMetadataInterval, + notificationBannersRefreshInterval: options.ServiceBannerRefreshInterval, + sshMaxTimeout: options.SSHMaxTimeout, + subsystems: options.Subsystems, + addresses: options.Addresses, + syscaller: options.Syscaller, + modifiedProcs: options.ModifiedProcesses, + processManagementTick: options.ProcessManagementTick, + logSender: agentsdk.NewLogSender(options.Logger), prometheusRegistry: prometheusRegistry, metrics: newAgentMetrics(prometheusRegistry), @@ -193,7 +193,7 @@ func New(options Options) Agent { // that gets closed on disconnection. This is used to wait for graceful disconnection from the // coordinator during shut down. close(a.coordDisconnected) - a.serviceBanner.Store(new(codersdk.ServiceBannerConfig)) + a.notificationBanners.Store(new([]codersdk.BannerConfig)) a.sessionToken.Store(new(string)) a.init() return a @@ -231,14 +231,14 @@ type agent struct { environmentVariables map[string]string - manifest atomic.Pointer[agentsdk.Manifest] // manifest is atomic because values can change after reconnection. - reportMetadataInterval time.Duration - scriptRunner *agentscripts.Runner - serviceBanner atomic.Pointer[codersdk.ServiceBannerConfig] // serviceBanner is atomic because it is periodically updated. - serviceBannerRefreshInterval time.Duration - sessionToken atomic.Pointer[string] - sshServer *agentssh.Server - sshMaxTimeout time.Duration + manifest atomic.Pointer[agentsdk.Manifest] // manifest is atomic because values can change after reconnection. + reportMetadataInterval time.Duration + scriptRunner *agentscripts.Runner + notificationBanners atomic.Pointer[[]codersdk.BannerConfig] // notificationBanners is atomic because it is periodically updated. + notificationBannersRefreshInterval time.Duration + sessionToken atomic.Pointer[string] + sshServer *agentssh.Server + sshMaxTimeout time.Duration lifecycleUpdate chan struct{} lifecycleReported chan codersdk.WorkspaceAgentLifecycle @@ -272,11 +272,11 @@ func (a *agent) TailnetConn() *tailnet.Conn { func (a *agent) init() { // pass the "hard" context because we explicitly close the SSH server as part of graceful shutdown. sshSrv, err := agentssh.NewServer(a.hardCtx, a.logger.Named("ssh-server"), a.prometheusRegistry, a.filesystem, &agentssh.Config{ - MaxTimeout: a.sshMaxTimeout, - MOTDFile: func() string { return a.manifest.Load().MOTDFile }, - ServiceBanner: func() *codersdk.ServiceBannerConfig { return a.serviceBanner.Load() }, - UpdateEnv: a.updateCommandEnv, - WorkingDirectory: func() string { return a.manifest.Load().Directory }, + MaxTimeout: a.sshMaxTimeout, + MOTDFile: func() string { return a.manifest.Load().MOTDFile }, + NotificationBanners: func() *[]codersdk.BannerConfig { return a.notificationBanners.Load() }, + UpdateEnv: a.updateCommandEnv, + WorkingDirectory: func() string { return a.manifest.Load().Directory }, }) if err != nil { panic(err) @@ -709,23 +709,26 @@ func (a *agent) setLifecycle(state codersdk.WorkspaceAgentLifecycle) { // (and must be done before the session actually starts). func (a *agent) fetchServiceBannerLoop(ctx context.Context, conn drpc.Conn) error { aAPI := proto.NewDRPCAgentClient(conn) - ticker := time.NewTicker(a.serviceBannerRefreshInterval) + ticker := time.NewTicker(a.notificationBannersRefreshInterval) defer ticker.Stop() for { select { case <-ctx.Done(): return ctx.Err() case <-ticker.C: - sbp, err := aAPI.GetServiceBanner(ctx, &proto.GetServiceBannerRequest{}) + bannersProto, err := aAPI.GetNotificationBanners(ctx, &proto.GetNotificationBannersRequest{}) if err != nil { if ctx.Err() != nil { return ctx.Err() } - a.logger.Error(ctx, "failed to update service banner", slog.Error(err)) + a.logger.Error(ctx, "failed to update notification banners", slog.Error(err)) return err } - serviceBanner := agentsdk.ServiceBannerFromProto(sbp) - a.serviceBanner.Store(&serviceBanner) + banners := make([]codersdk.BannerConfig, 0, len(bannersProto.NotificationBanners)) + for _, bannerProto := range bannersProto.NotificationBanners { + banners = append(banners, agentsdk.BannerConfigFromProto(bannerProto)) + } + a.notificationBanners.Store(&banners) } } } @@ -757,15 +760,18 @@ func (a *agent) run() (retErr error) { // redial the coder server and retry. connMan := newAPIConnRoutineManager(a.gracefulCtx, a.hardCtx, a.logger, conn) - connMan.start("init service banner", gracefulShutdownBehaviorStop, + connMan.start("init notification banners", gracefulShutdownBehaviorStop, func(ctx context.Context, conn drpc.Conn) error { aAPI := proto.NewDRPCAgentClient(conn) - sbp, err := aAPI.GetServiceBanner(ctx, &proto.GetServiceBannerRequest{}) + bannersProto, err := aAPI.GetNotificationBanners(ctx, &proto.GetNotificationBannersRequest{}) if err != nil { return xerrors.Errorf("fetch service banner: %w", err) } - serviceBanner := agentsdk.ServiceBannerFromProto(sbp) - a.serviceBanner.Store(&serviceBanner) + banners := make([]codersdk.BannerConfig, 0, len(bannersProto.NotificationBanners)) + for _, bannerProto := range bannersProto.NotificationBanners { + banners = append(banners, agentsdk.BannerConfigFromProto(bannerProto)) + } + a.notificationBanners.Store(&banners) return nil }, ) diff --git a/agent/agent_test.go b/agent/agent_test.go index 45ebf7b709199..c674a29ec35f6 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -614,12 +614,12 @@ func TestAgent_Session_TTY_MOTD_Update(t *testing.T) { // Set new banner func and wait for the agent to call it to update the // banner. ready := make(chan struct{}, 2) - client.SetServiceBannerFunc(func() (codersdk.ServiceBannerConfig, error) { + client.SetNotificationBannersFunc(func() ([]codersdk.BannerConfig, error) { select { case ready <- struct{}{}: default: } - return test.banner, nil + return []codersdk.BannerConfig{test.banner}, nil }) <-ready <-ready // Wait for two updates to ensure the value has propagated. @@ -2193,15 +2193,15 @@ func setupAgentSSHClient(ctx context.Context, t *testing.T) *ssh.Client { func setupSSHSession( t *testing.T, manifest agentsdk.Manifest, - serviceBanner codersdk.ServiceBannerConfig, + banner codersdk.BannerConfig, prepareFS func(fs afero.Fs), opts ...func(*agenttest.Client, *agent.Options), ) *ssh.Session { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() opts = append(opts, func(c *agenttest.Client, o *agent.Options) { - c.SetServiceBannerFunc(func() (codersdk.ServiceBannerConfig, error) { - return serviceBanner, nil + c.SetNotificationBannersFunc(func() ([]codersdk.BannerConfig, error) { + return []codersdk.BannerConfig{banner}, nil }) }) //nolint:dogsled diff --git a/agent/agentssh/agentssh.go b/agent/agentssh/agentssh.go index 48da6aa0296ca..4fcc6ab869c5b 100644 --- a/agent/agentssh/agentssh.go +++ b/agent/agentssh/agentssh.go @@ -63,7 +63,7 @@ type Config struct { // file will be displayed to the user upon login. MOTDFile func() string // ServiceBanner returns the configuration for the Coder service banner. - ServiceBanner func() *codersdk.ServiceBannerConfig + NotificationBanners func() *[]codersdk.BannerConfig // UpdateEnv updates the environment variables for the command to be // executed. It can be used to add, modify or replace environment variables. UpdateEnv func(current []string) (updated []string, err error) @@ -123,8 +123,8 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom if config.MOTDFile == nil { config.MOTDFile = func() string { return "" } } - if config.ServiceBanner == nil { - config.ServiceBanner = func() *codersdk.ServiceBannerConfig { return &codersdk.ServiceBannerConfig{} } + if config.NotificationBanners == nil { + config.NotificationBanners = func() *[]codersdk.BannerConfig { return &[]codersdk.BannerConfig{} } } if config.WorkingDirectory == nil { config.WorkingDirectory = func() string { @@ -441,12 +441,15 @@ func (s *Server) startPTYSession(logger slog.Logger, session ptySession, magicTy session.DisablePTYEmulation() if isLoginShell(session.RawCommand()) { - serviceBanner := s.config.ServiceBanner() - if serviceBanner != nil { - err := showServiceBanner(session, serviceBanner) - if err != nil { - logger.Error(ctx, "agent failed to show service banner", slog.Error(err)) - s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "service_banner").Add(1) + banners := s.config.NotificationBanners() + if banners != nil { + for _, banner := range *banners { + err := showNotificationBanner(session, banner) + if err != nil { + logger.Error(ctx, "agent failed to show service banner", slog.Error(err)) + s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "notification_banner").Add(1) + break + } } } } @@ -891,9 +894,9 @@ func isQuietLogin(fs afero.Fs, rawCommand string) bool { return err == nil } -// showServiceBanner will write the service banner if enabled and not blank +// showNotificationBanner will write the service banner if enabled and not blank // along with a blank line for spacing. -func showServiceBanner(session io.Writer, banner *codersdk.ServiceBannerConfig) error { +func showNotificationBanner(session io.Writer, banner codersdk.BannerConfig) error { if banner.Enabled && banner.Message != "" { // The banner supports Markdown so we might want to parse it but Markdown is // still fairly readable in its raw form. diff --git a/agent/agenttest/client.go b/agent/agenttest/client.go index 22eba14483f92..b21a7444c6084 100644 --- a/agent/agenttest/client.go +++ b/agent/agenttest/client.go @@ -138,8 +138,8 @@ func (c *Client) GetStartupLogs() []agentsdk.Log { return c.logs } -func (c *Client) SetServiceBannerFunc(f func() (codersdk.ServiceBannerConfig, error)) { - c.fakeAgentAPI.SetServiceBannerFunc(f) +func (c *Client) SetNotificationBannersFunc(f func() ([]codersdk.ServiceBannerConfig, error)) { + c.fakeAgentAPI.SetNotificationBannersFunc(f) } func (c *Client) PushDERPMapUpdate(update *tailcfg.DERPMap) error { @@ -171,31 +171,39 @@ type FakeAgentAPI struct { lifecycleStates []codersdk.WorkspaceAgentLifecycle metadata map[string]agentsdk.Metadata - getServiceBannerFunc func() (codersdk.ServiceBannerConfig, error) + getNotificationBannersFunc func() ([]codersdk.BannerConfig, error) } func (f *FakeAgentAPI) GetManifest(context.Context, *agentproto.GetManifestRequest) (*agentproto.Manifest, error) { return f.manifest, nil } -func (f *FakeAgentAPI) SetServiceBannerFunc(fn func() (codersdk.ServiceBannerConfig, error)) { +func (*FakeAgentAPI) GetServiceBanner(context.Context, *agentproto.GetServiceBannerRequest) (*agentproto.ServiceBanner, error) { + return &agentproto.ServiceBanner{}, nil +} + +func (f *FakeAgentAPI) SetNotificationBannersFunc(fn func() ([]codersdk.BannerConfig, error)) { f.Lock() defer f.Unlock() - f.getServiceBannerFunc = fn - f.logger.Info(context.Background(), "updated ServiceBannerFunc") + f.getNotificationBannersFunc = fn + f.logger.Info(context.Background(), "updated notification banners") } -func (f *FakeAgentAPI) GetServiceBanner(context.Context, *agentproto.GetServiceBannerRequest) (*agentproto.ServiceBanner, error) { +func (f *FakeAgentAPI) GetNotificationBanners(context.Context, *agentproto.GetNotificationBannersRequest) (*agentproto.GetNotificationBannersResponse, error) { f.Lock() defer f.Unlock() - if f.getServiceBannerFunc == nil { - return &agentproto.ServiceBanner{}, nil + if f.getNotificationBannersFunc == nil { + return &agentproto.GetNotificationBannersResponse{NotificationBanners: []*agentproto.BannerConfig{}}, nil } - sb, err := f.getServiceBannerFunc() + banners, err := f.getNotificationBannersFunc() if err != nil { return nil, err } - return agentsdk.ProtoFromServiceBanner(sb), nil + bannersProto := make([]*agentproto.BannerConfig, 0, len(banners)) + for _, banner := range banners { + bannersProto = append(bannersProto, agentsdk.ProtoFromBannerConfig(banner)) + } + return &agentproto.GetNotificationBannersResponse{NotificationBanners: bannersProto}, nil } func (f *FakeAgentAPI) UpdateStats(ctx context.Context, req *agentproto.UpdateStatsRequest) (*agentproto.UpdateStatsResponse, error) { diff --git a/agent/proto/agent.pb.go b/agent/proto/agent.pb.go index 20bd20460275f..41e8d061054a5 100644 --- a/agent/proto/agent.pb.go +++ b/agent/proto/agent.pb.go @@ -1859,6 +1859,154 @@ func (x *BatchCreateLogsResponse) GetLogLimitExceeded() bool { return false } +type GetNotificationBannersRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *GetNotificationBannersRequest) Reset() { + *x = GetNotificationBannersRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetNotificationBannersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetNotificationBannersRequest) ProtoMessage() {} + +func (x *GetNotificationBannersRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[22] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetNotificationBannersRequest.ProtoReflect.Descriptor instead. +func (*GetNotificationBannersRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{22} +} + +type GetNotificationBannersResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + NotificationBanners []*BannerConfig `protobuf:"bytes,1,rep,name=notification_banners,json=notificationBanners,proto3" json:"notification_banners,omitempty"` +} + +func (x *GetNotificationBannersResponse) Reset() { + *x = GetNotificationBannersResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetNotificationBannersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetNotificationBannersResponse) ProtoMessage() {} + +func (x *GetNotificationBannersResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[23] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetNotificationBannersResponse.ProtoReflect.Descriptor instead. +func (*GetNotificationBannersResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{23} +} + +func (x *GetNotificationBannersResponse) GetNotificationBanners() []*BannerConfig { + if x != nil { + return x.NotificationBanners + } + return nil +} + +type BannerConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + BackgroundColor string `protobuf:"bytes,3,opt,name=background_color,json=backgroundColor,proto3" json:"background_color,omitempty"` +} + +func (x *BannerConfig) Reset() { + *x = BannerConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BannerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BannerConfig) ProtoMessage() {} + +func (x *BannerConfig) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[24] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BannerConfig.ProtoReflect.Descriptor instead. +func (*BannerConfig) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{24} +} + +func (x *BannerConfig) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *BannerConfig) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *BannerConfig) GetBackgroundColor() string { + if x != nil { + return x.BackgroundColor + } + return "" +} + type WorkspaceApp_Healthcheck struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1872,7 +2020,7 @@ type WorkspaceApp_Healthcheck struct { func (x *WorkspaceApp_Healthcheck) Reset() { *x = WorkspaceApp_Healthcheck{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[22] + mi := &file_agent_proto_agent_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1885,7 +2033,7 @@ func (x *WorkspaceApp_Healthcheck) String() string { func (*WorkspaceApp_Healthcheck) ProtoMessage() {} func (x *WorkspaceApp_Healthcheck) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[22] + mi := &file_agent_proto_agent_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1936,7 +2084,7 @@ type WorkspaceAgentMetadata_Result struct { func (x *WorkspaceAgentMetadata_Result) Reset() { *x = WorkspaceAgentMetadata_Result{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[23] + mi := &file_agent_proto_agent_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1949,7 +2097,7 @@ func (x *WorkspaceAgentMetadata_Result) String() string { func (*WorkspaceAgentMetadata_Result) ProtoMessage() {} func (x *WorkspaceAgentMetadata_Result) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[23] + mi := &file_agent_proto_agent_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2008,7 +2156,7 @@ type WorkspaceAgentMetadata_Description struct { func (x *WorkspaceAgentMetadata_Description) Reset() { *x = WorkspaceAgentMetadata_Description{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[24] + mi := &file_agent_proto_agent_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2021,7 +2169,7 @@ func (x *WorkspaceAgentMetadata_Description) String() string { func (*WorkspaceAgentMetadata_Description) ProtoMessage() {} func (x *WorkspaceAgentMetadata_Description) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[24] + mi := &file_agent_proto_agent_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2086,7 +2234,7 @@ type Stats_Metric struct { func (x *Stats_Metric) Reset() { *x = Stats_Metric{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[27] + mi := &file_agent_proto_agent_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2099,7 +2247,7 @@ func (x *Stats_Metric) String() string { func (*Stats_Metric) ProtoMessage() {} func (x *Stats_Metric) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[27] + mi := &file_agent_proto_agent_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2155,7 +2303,7 @@ type Stats_Metric_Label struct { func (x *Stats_Metric_Label) Reset() { *x = Stats_Metric_Label{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[28] + mi := &file_agent_proto_agent_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2168,7 +2316,7 @@ func (x *Stats_Metric_Label) String() string { func (*Stats_Metric_Label) ProtoMessage() {} func (x *Stats_Metric_Label) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[28] + mi := &file_agent_proto_agent_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2210,7 +2358,7 @@ type BatchUpdateAppHealthRequest_HealthUpdate struct { func (x *BatchUpdateAppHealthRequest_HealthUpdate) Reset() { *x = BatchUpdateAppHealthRequest_HealthUpdate{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[29] + mi := &file_agent_proto_agent_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2223,7 +2371,7 @@ func (x *BatchUpdateAppHealthRequest_HealthUpdate) String() string { func (*BatchUpdateAppHealthRequest_HealthUpdate) ProtoMessage() {} func (x *BatchUpdateAppHealthRequest_HealthUpdate) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[29] + mi := &file_agent_proto_agent_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2594,64 +2742,87 @@ var file_agent_proto_agent_proto_rawDesc = []byte{ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x5f, 0x65, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x6c, 0x6f, 0x67, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x45, 0x78, 0x63, 0x65, - 0x65, 0x64, 0x65, 0x64, 0x2a, 0x63, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, - 0x68, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x50, 0x50, 0x5f, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x5f, - 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, - 0x08, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x49, - 0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0b, 0x0a, - 0x07, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, - 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x04, 0x32, 0xf6, 0x05, 0x0a, 0x05, 0x41, 0x67, - 0x65, 0x6e, 0x74, 0x12, 0x4b, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, - 0x73, 0x74, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, - 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, - 0x12, 0x5a, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, - 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x56, 0x0a, 0x0b, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, - 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, - 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, - 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, - 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x42, 0x61, - 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, - 0x74, 0x68, 0x73, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, + 0x65, 0x64, 0x65, 0x64, 0x22, 0x1f, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x71, 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x14, 0x6e, 0x6f, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x62, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x13, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x22, 0x6d, 0x0a, 0x0c, 0x42, 0x61, 0x6e, 0x6e, + 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, 0x10, + 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, + 0x6e, 0x64, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x2a, 0x63, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x48, 0x65, + 0x61, 0x6c, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x50, 0x50, 0x5f, 0x48, 0x45, 0x41, 0x4c, + 0x54, 0x48, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, + 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10, + 0x0a, 0x0c, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02, + 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a, + 0x09, 0x55, 0x4e, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x04, 0x32, 0xef, 0x06, 0x0a, + 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x4b, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, + 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, + 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, + 0x65, 0x73, 0x74, 0x12, 0x5a, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, + 0x56, 0x0a, 0x0b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x22, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x72, 0x0a, + 0x15, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, + 0x65, 0x61, 0x6c, 0x74, 0x68, 0x73, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, - 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, - 0x0a, 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, - 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, - 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x6e, - 0x0a, 0x13, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, - 0x0a, 0x0f, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, - 0x73, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, - 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x4e, 0x0a, 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, + 0x75, 0x70, 0x12, 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, + 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, + 0x70, 0x12, 0x6e, 0x0a, 0x13, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x62, 0x0a, 0x0f, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, + 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x12, + 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, + 0x2e, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, + 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x27, + 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2667,7 +2838,7 @@ func file_agent_proto_agent_proto_rawDescGZIP() []byte { } var file_agent_proto_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 7) -var file_agent_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 30) +var file_agent_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 33) var file_agent_proto_agent_proto_goTypes = []interface{}{ (AppHealth)(0), // 0: coder.agent.v2.AppHealth (WorkspaceApp_SharingLevel)(0), // 1: coder.agent.v2.WorkspaceApp.SharingLevel @@ -2698,73 +2869,79 @@ var file_agent_proto_agent_proto_goTypes = []interface{}{ (*Log)(nil), // 26: coder.agent.v2.Log (*BatchCreateLogsRequest)(nil), // 27: coder.agent.v2.BatchCreateLogsRequest (*BatchCreateLogsResponse)(nil), // 28: coder.agent.v2.BatchCreateLogsResponse - (*WorkspaceApp_Healthcheck)(nil), // 29: coder.agent.v2.WorkspaceApp.Healthcheck - (*WorkspaceAgentMetadata_Result)(nil), // 30: coder.agent.v2.WorkspaceAgentMetadata.Result - (*WorkspaceAgentMetadata_Description)(nil), // 31: coder.agent.v2.WorkspaceAgentMetadata.Description - nil, // 32: coder.agent.v2.Manifest.EnvironmentVariablesEntry - nil, // 33: coder.agent.v2.Stats.ConnectionsByProtoEntry - (*Stats_Metric)(nil), // 34: coder.agent.v2.Stats.Metric - (*Stats_Metric_Label)(nil), // 35: coder.agent.v2.Stats.Metric.Label - (*BatchUpdateAppHealthRequest_HealthUpdate)(nil), // 36: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate - (*durationpb.Duration)(nil), // 37: google.protobuf.Duration - (*proto.DERPMap)(nil), // 38: coder.tailnet.v2.DERPMap - (*timestamppb.Timestamp)(nil), // 39: google.protobuf.Timestamp + (*GetNotificationBannersRequest)(nil), // 29: coder.agent.v2.GetNotificationBannersRequest + (*GetNotificationBannersResponse)(nil), // 30: coder.agent.v2.GetNotificationBannersResponse + (*BannerConfig)(nil), // 31: coder.agent.v2.BannerConfig + (*WorkspaceApp_Healthcheck)(nil), // 32: coder.agent.v2.WorkspaceApp.Healthcheck + (*WorkspaceAgentMetadata_Result)(nil), // 33: coder.agent.v2.WorkspaceAgentMetadata.Result + (*WorkspaceAgentMetadata_Description)(nil), // 34: coder.agent.v2.WorkspaceAgentMetadata.Description + nil, // 35: coder.agent.v2.Manifest.EnvironmentVariablesEntry + nil, // 36: coder.agent.v2.Stats.ConnectionsByProtoEntry + (*Stats_Metric)(nil), // 37: coder.agent.v2.Stats.Metric + (*Stats_Metric_Label)(nil), // 38: coder.agent.v2.Stats.Metric.Label + (*BatchUpdateAppHealthRequest_HealthUpdate)(nil), // 39: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate + (*durationpb.Duration)(nil), // 40: google.protobuf.Duration + (*proto.DERPMap)(nil), // 41: coder.tailnet.v2.DERPMap + (*timestamppb.Timestamp)(nil), // 42: google.protobuf.Timestamp } var file_agent_proto_agent_proto_depIdxs = []int32{ 1, // 0: coder.agent.v2.WorkspaceApp.sharing_level:type_name -> coder.agent.v2.WorkspaceApp.SharingLevel - 29, // 1: coder.agent.v2.WorkspaceApp.healthcheck:type_name -> coder.agent.v2.WorkspaceApp.Healthcheck + 32, // 1: coder.agent.v2.WorkspaceApp.healthcheck:type_name -> coder.agent.v2.WorkspaceApp.Healthcheck 2, // 2: coder.agent.v2.WorkspaceApp.health:type_name -> coder.agent.v2.WorkspaceApp.Health - 37, // 3: coder.agent.v2.WorkspaceAgentScript.timeout:type_name -> google.protobuf.Duration - 30, // 4: coder.agent.v2.WorkspaceAgentMetadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result - 31, // 5: coder.agent.v2.WorkspaceAgentMetadata.description:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description - 32, // 6: coder.agent.v2.Manifest.environment_variables:type_name -> coder.agent.v2.Manifest.EnvironmentVariablesEntry - 38, // 7: coder.agent.v2.Manifest.derp_map:type_name -> coder.tailnet.v2.DERPMap + 40, // 3: coder.agent.v2.WorkspaceAgentScript.timeout:type_name -> google.protobuf.Duration + 33, // 4: coder.agent.v2.WorkspaceAgentMetadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result + 34, // 5: coder.agent.v2.WorkspaceAgentMetadata.description:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description + 35, // 6: coder.agent.v2.Manifest.environment_variables:type_name -> coder.agent.v2.Manifest.EnvironmentVariablesEntry + 41, // 7: coder.agent.v2.Manifest.derp_map:type_name -> coder.tailnet.v2.DERPMap 8, // 8: coder.agent.v2.Manifest.scripts:type_name -> coder.agent.v2.WorkspaceAgentScript 7, // 9: coder.agent.v2.Manifest.apps:type_name -> coder.agent.v2.WorkspaceApp - 31, // 10: coder.agent.v2.Manifest.metadata:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description - 33, // 11: coder.agent.v2.Stats.connections_by_proto:type_name -> coder.agent.v2.Stats.ConnectionsByProtoEntry - 34, // 12: coder.agent.v2.Stats.metrics:type_name -> coder.agent.v2.Stats.Metric + 34, // 10: coder.agent.v2.Manifest.metadata:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description + 36, // 11: coder.agent.v2.Stats.connections_by_proto:type_name -> coder.agent.v2.Stats.ConnectionsByProtoEntry + 37, // 12: coder.agent.v2.Stats.metrics:type_name -> coder.agent.v2.Stats.Metric 14, // 13: coder.agent.v2.UpdateStatsRequest.stats:type_name -> coder.agent.v2.Stats - 37, // 14: coder.agent.v2.UpdateStatsResponse.report_interval:type_name -> google.protobuf.Duration + 40, // 14: coder.agent.v2.UpdateStatsResponse.report_interval:type_name -> google.protobuf.Duration 4, // 15: coder.agent.v2.Lifecycle.state:type_name -> coder.agent.v2.Lifecycle.State - 39, // 16: coder.agent.v2.Lifecycle.changed_at:type_name -> google.protobuf.Timestamp + 42, // 16: coder.agent.v2.Lifecycle.changed_at:type_name -> google.protobuf.Timestamp 17, // 17: coder.agent.v2.UpdateLifecycleRequest.lifecycle:type_name -> coder.agent.v2.Lifecycle - 36, // 18: coder.agent.v2.BatchUpdateAppHealthRequest.updates:type_name -> coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate + 39, // 18: coder.agent.v2.BatchUpdateAppHealthRequest.updates:type_name -> coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate 5, // 19: coder.agent.v2.Startup.subsystems:type_name -> coder.agent.v2.Startup.Subsystem 21, // 20: coder.agent.v2.UpdateStartupRequest.startup:type_name -> coder.agent.v2.Startup - 30, // 21: coder.agent.v2.Metadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result + 33, // 21: coder.agent.v2.Metadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result 23, // 22: coder.agent.v2.BatchUpdateMetadataRequest.metadata:type_name -> coder.agent.v2.Metadata - 39, // 23: coder.agent.v2.Log.created_at:type_name -> google.protobuf.Timestamp + 42, // 23: coder.agent.v2.Log.created_at:type_name -> google.protobuf.Timestamp 6, // 24: coder.agent.v2.Log.level:type_name -> coder.agent.v2.Log.Level 26, // 25: coder.agent.v2.BatchCreateLogsRequest.logs:type_name -> coder.agent.v2.Log - 37, // 26: coder.agent.v2.WorkspaceApp.Healthcheck.interval:type_name -> google.protobuf.Duration - 39, // 27: coder.agent.v2.WorkspaceAgentMetadata.Result.collected_at:type_name -> google.protobuf.Timestamp - 37, // 28: coder.agent.v2.WorkspaceAgentMetadata.Description.interval:type_name -> google.protobuf.Duration - 37, // 29: coder.agent.v2.WorkspaceAgentMetadata.Description.timeout:type_name -> google.protobuf.Duration - 3, // 30: coder.agent.v2.Stats.Metric.type:type_name -> coder.agent.v2.Stats.Metric.Type - 35, // 31: coder.agent.v2.Stats.Metric.labels:type_name -> coder.agent.v2.Stats.Metric.Label - 0, // 32: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate.health:type_name -> coder.agent.v2.AppHealth - 11, // 33: coder.agent.v2.Agent.GetManifest:input_type -> coder.agent.v2.GetManifestRequest - 13, // 34: coder.agent.v2.Agent.GetServiceBanner:input_type -> coder.agent.v2.GetServiceBannerRequest - 15, // 35: coder.agent.v2.Agent.UpdateStats:input_type -> coder.agent.v2.UpdateStatsRequest - 18, // 36: coder.agent.v2.Agent.UpdateLifecycle:input_type -> coder.agent.v2.UpdateLifecycleRequest - 19, // 37: coder.agent.v2.Agent.BatchUpdateAppHealths:input_type -> coder.agent.v2.BatchUpdateAppHealthRequest - 22, // 38: coder.agent.v2.Agent.UpdateStartup:input_type -> coder.agent.v2.UpdateStartupRequest - 24, // 39: coder.agent.v2.Agent.BatchUpdateMetadata:input_type -> coder.agent.v2.BatchUpdateMetadataRequest - 27, // 40: coder.agent.v2.Agent.BatchCreateLogs:input_type -> coder.agent.v2.BatchCreateLogsRequest - 10, // 41: coder.agent.v2.Agent.GetManifest:output_type -> coder.agent.v2.Manifest - 12, // 42: coder.agent.v2.Agent.GetServiceBanner:output_type -> coder.agent.v2.ServiceBanner - 16, // 43: coder.agent.v2.Agent.UpdateStats:output_type -> coder.agent.v2.UpdateStatsResponse - 17, // 44: coder.agent.v2.Agent.UpdateLifecycle:output_type -> coder.agent.v2.Lifecycle - 20, // 45: coder.agent.v2.Agent.BatchUpdateAppHealths:output_type -> coder.agent.v2.BatchUpdateAppHealthResponse - 21, // 46: coder.agent.v2.Agent.UpdateStartup:output_type -> coder.agent.v2.Startup - 25, // 47: coder.agent.v2.Agent.BatchUpdateMetadata:output_type -> coder.agent.v2.BatchUpdateMetadataResponse - 28, // 48: coder.agent.v2.Agent.BatchCreateLogs:output_type -> coder.agent.v2.BatchCreateLogsResponse - 41, // [41:49] is the sub-list for method output_type - 33, // [33:41] is the sub-list for method input_type - 33, // [33:33] is the sub-list for extension type_name - 33, // [33:33] is the sub-list for extension extendee - 0, // [0:33] is the sub-list for field type_name + 31, // 26: coder.agent.v2.GetNotificationBannersResponse.notification_banners:type_name -> coder.agent.v2.BannerConfig + 40, // 27: coder.agent.v2.WorkspaceApp.Healthcheck.interval:type_name -> google.protobuf.Duration + 42, // 28: coder.agent.v2.WorkspaceAgentMetadata.Result.collected_at:type_name -> google.protobuf.Timestamp + 40, // 29: coder.agent.v2.WorkspaceAgentMetadata.Description.interval:type_name -> google.protobuf.Duration + 40, // 30: coder.agent.v2.WorkspaceAgentMetadata.Description.timeout:type_name -> google.protobuf.Duration + 3, // 31: coder.agent.v2.Stats.Metric.type:type_name -> coder.agent.v2.Stats.Metric.Type + 38, // 32: coder.agent.v2.Stats.Metric.labels:type_name -> coder.agent.v2.Stats.Metric.Label + 0, // 33: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate.health:type_name -> coder.agent.v2.AppHealth + 11, // 34: coder.agent.v2.Agent.GetManifest:input_type -> coder.agent.v2.GetManifestRequest + 13, // 35: coder.agent.v2.Agent.GetServiceBanner:input_type -> coder.agent.v2.GetServiceBannerRequest + 15, // 36: coder.agent.v2.Agent.UpdateStats:input_type -> coder.agent.v2.UpdateStatsRequest + 18, // 37: coder.agent.v2.Agent.UpdateLifecycle:input_type -> coder.agent.v2.UpdateLifecycleRequest + 19, // 38: coder.agent.v2.Agent.BatchUpdateAppHealths:input_type -> coder.agent.v2.BatchUpdateAppHealthRequest + 22, // 39: coder.agent.v2.Agent.UpdateStartup:input_type -> coder.agent.v2.UpdateStartupRequest + 24, // 40: coder.agent.v2.Agent.BatchUpdateMetadata:input_type -> coder.agent.v2.BatchUpdateMetadataRequest + 27, // 41: coder.agent.v2.Agent.BatchCreateLogs:input_type -> coder.agent.v2.BatchCreateLogsRequest + 29, // 42: coder.agent.v2.Agent.GetNotificationBanners:input_type -> coder.agent.v2.GetNotificationBannersRequest + 10, // 43: coder.agent.v2.Agent.GetManifest:output_type -> coder.agent.v2.Manifest + 12, // 44: coder.agent.v2.Agent.GetServiceBanner:output_type -> coder.agent.v2.ServiceBanner + 16, // 45: coder.agent.v2.Agent.UpdateStats:output_type -> coder.agent.v2.UpdateStatsResponse + 17, // 46: coder.agent.v2.Agent.UpdateLifecycle:output_type -> coder.agent.v2.Lifecycle + 20, // 47: coder.agent.v2.Agent.BatchUpdateAppHealths:output_type -> coder.agent.v2.BatchUpdateAppHealthResponse + 21, // 48: coder.agent.v2.Agent.UpdateStartup:output_type -> coder.agent.v2.Startup + 25, // 49: coder.agent.v2.Agent.BatchUpdateMetadata:output_type -> coder.agent.v2.BatchUpdateMetadataResponse + 28, // 50: coder.agent.v2.Agent.BatchCreateLogs:output_type -> coder.agent.v2.BatchCreateLogsResponse + 30, // 51: coder.agent.v2.Agent.GetNotificationBanners:output_type -> coder.agent.v2.GetNotificationBannersResponse + 43, // [43:52] is the sub-list for method output_type + 34, // [34:43] is the sub-list for method input_type + 34, // [34:34] is the sub-list for extension type_name + 34, // [34:34] is the sub-list for extension extendee + 0, // [0:34] is the sub-list for field type_name } func init() { file_agent_proto_agent_proto_init() } @@ -3038,7 +3215,7 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WorkspaceApp_Healthcheck); i { + switch v := v.(*GetNotificationBannersRequest); i { case 0: return &v.state case 1: @@ -3050,7 +3227,7 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WorkspaceAgentMetadata_Result); i { + switch v := v.(*GetNotificationBannersResponse); i { case 0: return &v.state case 1: @@ -3062,7 +3239,31 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WorkspaceAgentMetadata_Description); i { + switch v := v.(*BannerConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_proto_agent_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WorkspaceApp_Healthcheck); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_proto_agent_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WorkspaceAgentMetadata_Result); i { case 0: return &v.state case 1: @@ -3074,6 +3275,18 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WorkspaceAgentMetadata_Description); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_proto_agent_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Stats_Metric); i { case 0: return &v.state @@ -3085,7 +3298,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Stats_Metric_Label); i { case 0: return &v.state @@ -3097,7 +3310,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*BatchUpdateAppHealthRequest_HealthUpdate); i { case 0: return &v.state @@ -3116,7 +3329,7 @@ func file_agent_proto_agent_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_agent_proto_agent_proto_rawDesc, NumEnums: 7, - NumMessages: 30, + NumMessages: 33, NumExtensions: 0, NumServices: 1, }, diff --git a/agent/proto/agent.proto b/agent/proto/agent.proto index f09c836446a04..8432fe8ef7f2b 100644 --- a/agent/proto/agent.proto +++ b/agent/proto/agent.proto @@ -251,6 +251,18 @@ message BatchCreateLogsResponse { bool log_limit_exceeded = 1; } +message GetNotificationBannersRequest {} + +message GetNotificationBannersResponse { + repeated BannerConfig notification_banners = 1; +} + +message BannerConfig { + bool enabled = 1; + string message = 2; + string background_color = 3; +} + service Agent { rpc GetManifest(GetManifestRequest) returns (Manifest); rpc GetServiceBanner(GetServiceBannerRequest) returns (ServiceBanner); @@ -260,4 +272,5 @@ service Agent { rpc UpdateStartup(UpdateStartupRequest) returns (Startup); rpc BatchUpdateMetadata(BatchUpdateMetadataRequest) returns (BatchUpdateMetadataResponse); rpc BatchCreateLogs(BatchCreateLogsRequest) returns (BatchCreateLogsResponse); + rpc GetNotificationBanners(GetNotificationBannersRequest) returns (GetNotificationBannersResponse); } diff --git a/agent/proto/agent_drpc.pb.go b/agent/proto/agent_drpc.pb.go index 4bbf980522dd1..0003a1fa4568a 100644 --- a/agent/proto/agent_drpc.pb.go +++ b/agent/proto/agent_drpc.pb.go @@ -46,6 +46,7 @@ type DRPCAgentClient interface { UpdateStartup(ctx context.Context, in *UpdateStartupRequest) (*Startup, error) BatchUpdateMetadata(ctx context.Context, in *BatchUpdateMetadataRequest) (*BatchUpdateMetadataResponse, error) BatchCreateLogs(ctx context.Context, in *BatchCreateLogsRequest) (*BatchCreateLogsResponse, error) + GetNotificationBanners(ctx context.Context, in *GetNotificationBannersRequest) (*GetNotificationBannersResponse, error) } type drpcAgentClient struct { @@ -130,6 +131,15 @@ func (c *drpcAgentClient) BatchCreateLogs(ctx context.Context, in *BatchCreateLo return out, nil } +func (c *drpcAgentClient) GetNotificationBanners(ctx context.Context, in *GetNotificationBannersRequest) (*GetNotificationBannersResponse, error) { + out := new(GetNotificationBannersResponse) + err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/GetNotificationBanners", drpcEncoding_File_agent_proto_agent_proto{}, in, out) + if err != nil { + return nil, err + } + return out, nil +} + type DRPCAgentServer interface { GetManifest(context.Context, *GetManifestRequest) (*Manifest, error) GetServiceBanner(context.Context, *GetServiceBannerRequest) (*ServiceBanner, error) @@ -139,6 +149,7 @@ type DRPCAgentServer interface { UpdateStartup(context.Context, *UpdateStartupRequest) (*Startup, error) BatchUpdateMetadata(context.Context, *BatchUpdateMetadataRequest) (*BatchUpdateMetadataResponse, error) BatchCreateLogs(context.Context, *BatchCreateLogsRequest) (*BatchCreateLogsResponse, error) + GetNotificationBanners(context.Context, *GetNotificationBannersRequest) (*GetNotificationBannersResponse, error) } type DRPCAgentUnimplementedServer struct{} @@ -175,9 +186,13 @@ func (s *DRPCAgentUnimplementedServer) BatchCreateLogs(context.Context, *BatchCr return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) } +func (s *DRPCAgentUnimplementedServer) GetNotificationBanners(context.Context, *GetNotificationBannersRequest) (*GetNotificationBannersResponse, error) { + return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) +} + type DRPCAgentDescription struct{} -func (DRPCAgentDescription) NumMethods() int { return 8 } +func (DRPCAgentDescription) NumMethods() int { return 9 } func (DRPCAgentDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { switch n { @@ -253,6 +268,15 @@ func (DRPCAgentDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, in1.(*BatchCreateLogsRequest), ) }, DRPCAgentServer.BatchCreateLogs, true + case 8: + return "/coder.agent.v2.Agent/GetNotificationBanners", drpcEncoding_File_agent_proto_agent_proto{}, + func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { + return srv.(DRPCAgentServer). + GetNotificationBanners( + ctx, + in1.(*GetNotificationBannersRequest), + ) + }, DRPCAgentServer.GetNotificationBanners, true default: return "", nil, nil, nil, false } @@ -389,3 +413,19 @@ func (x *drpcAgent_BatchCreateLogsStream) SendAndClose(m *BatchCreateLogsRespons } return x.CloseSend() } + +type DRPCAgent_GetNotificationBannersStream interface { + drpc.Stream + SendAndClose(*GetNotificationBannersResponse) error +} + +type drpcAgent_GetNotificationBannersStream struct { + drpc.Stream +} + +func (x *drpcAgent_GetNotificationBannersStream) SendAndClose(m *GetNotificationBannersResponse) error { + if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil { + return err + } + return x.CloseSend() +} diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go index acfe9145b2ad0..fa8563a141a45 100644 --- a/coderd/agentapi/api.go +++ b/coderd/agentapi/api.go @@ -35,7 +35,7 @@ import ( type API struct { opts Options *ManifestAPI - *ServiceBannerAPI + *NotificationBannerAPI *StatsAPI *LifecycleAPI *AppsAPI @@ -107,7 +107,7 @@ func New(opts Options) *API { }, } - api.ServiceBannerAPI = &ServiceBannerAPI{ + api.NotificationBannerAPI = &NotificationBannerAPI{ appearanceFetcher: opts.AppearanceFetcher, } diff --git a/coderd/agentapi/notification_banners.go b/coderd/agentapi/notification_banners.go new file mode 100644 index 0000000000000..ab4e7dda96741 --- /dev/null +++ b/coderd/agentapi/notification_banners.go @@ -0,0 +1,39 @@ +package agentapi + +import ( + "context" + "sync/atomic" + + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/coderd/appearance" + "github.com/coder/coder/v2/codersdk/agentsdk" +) + +type NotificationBannerAPI struct { + appearanceFetcher *atomic.Pointer[appearance.Fetcher] +} + +// Deprecated: GetServiceBanner has been deprecated in favor of GetNotificationBanners. +func (a *NotificationBannerAPI) GetServiceBanner(ctx context.Context, _ *proto.GetServiceBannerRequest) (*proto.ServiceBanner, error) { + cfg, err := (*a.appearanceFetcher.Load()).Fetch(ctx) + if err != nil { + return nil, xerrors.Errorf("fetch appearance: %w", err) + } + return agentsdk.ProtoFromServiceBanner(cfg.ServiceBanner), nil +} + +func (a *NotificationBannerAPI) GetNotificationBanners(ctx context.Context, _ *proto.GetNotificationBannersRequest) (*proto.GetNotificationBannersResponse, error) { + cfg, err := (*a.appearanceFetcher.Load()).Fetch(ctx) + if err != nil { + return nil, xerrors.Errorf("fetch appearance: %w", err) + } + banners := make([]*proto.BannerConfig, 0, len(cfg.NotificationBanners)) + for _, banner := range cfg.NotificationBanners { + banners = append(banners, agentsdk.ProtoFromBannerConfig(banner)) + } + return &proto.GetNotificationBannersResponse{ + NotificationBanners: banners, + }, nil +} diff --git a/coderd/agentapi/servicebanner_internal_test.go b/coderd/agentapi/notification_banners_internal_test.go similarity index 57% rename from coderd/agentapi/servicebanner_internal_test.go rename to coderd/agentapi/notification_banners_internal_test.go index 6098d7df5f3d9..87f4df2d21764 100644 --- a/coderd/agentapi/servicebanner_internal_test.go +++ b/coderd/agentapi/notification_banners_internal_test.go @@ -11,36 +11,30 @@ import ( agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/appearance" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/agentsdk" ) -func TestGetServiceBanner(t *testing.T) { +func TestGetNotificationBanners(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { t.Parallel() - cfg := codersdk.ServiceBannerConfig{ + cfg := []codersdk.BannerConfig{{ Enabled: true, - Message: "hello world", - BackgroundColor: "#000000", - } + Message: "The beep-bop will be boop-beeped on Saturday at 12AM PST.", + BackgroundColor: "#00FF00", + }} - var ff appearance.Fetcher = fakeFetcher{cfg: codersdk.AppearanceConfig{ServiceBanner: cfg}} + var ff appearance.Fetcher = fakeFetcher{cfg: codersdk.AppearanceConfig{NotificationBanners: cfg}} ptr := atomic.Pointer[appearance.Fetcher]{} ptr.Store(&ff) - api := &ServiceBannerAPI{ - appearanceFetcher: &ptr, - } - - resp, err := api.GetServiceBanner(context.Background(), &agentproto.GetServiceBannerRequest{}) + api := &NotificationBannerAPI{appearanceFetcher: &ptr} + resp, err := api.GetNotificationBanners(context.Background(), &agentproto.GetNotificationBannersRequest{}) require.NoError(t, err) - - require.Equal(t, &agentproto.ServiceBanner{ - Enabled: cfg.Enabled, - Message: cfg.Message, - BackgroundColor: cfg.BackgroundColor, - }, resp) + require.Len(t, resp.NotificationBanners, 1) + require.Equal(t, cfg[0], agentsdk.BannerConfigFromProto(resp.NotificationBanners[0])) }) t.Run("FetchError", func(t *testing.T) { @@ -51,11 +45,8 @@ func TestGetServiceBanner(t *testing.T) { ptr := atomic.Pointer[appearance.Fetcher]{} ptr.Store(&ff) - api := &ServiceBannerAPI{ - appearanceFetcher: &ptr, - } - - resp, err := api.GetServiceBanner(context.Background(), &agentproto.GetServiceBannerRequest{}) + api := &NotificationBannerAPI{appearanceFetcher: &ptr} + resp, err := api.GetNotificationBanners(context.Background(), &agentproto.GetNotificationBannersRequest{}) require.Error(t, err) require.ErrorIs(t, err, expectedErr) require.Nil(t, resp) diff --git a/coderd/agentapi/servicebanner.go b/coderd/agentapi/servicebanner.go deleted file mode 100644 index 2e835003c79a4..0000000000000 --- a/coderd/agentapi/servicebanner.go +++ /dev/null @@ -1,24 +0,0 @@ -package agentapi - -import ( - "context" - "sync/atomic" - - "golang.org/x/xerrors" - - "github.com/coder/coder/v2/agent/proto" - "github.com/coder/coder/v2/coderd/appearance" - "github.com/coder/coder/v2/codersdk/agentsdk" -) - -type ServiceBannerAPI struct { - appearanceFetcher *atomic.Pointer[appearance.Fetcher] -} - -func (a *ServiceBannerAPI) GetServiceBanner(ctx context.Context, _ *proto.GetServiceBannerRequest) (*proto.ServiceBanner, error) { - cfg, err := (*a.appearanceFetcher.Load()).Fetch(ctx) - if err != nil { - return nil, xerrors.Errorf("fetch appearance: %w", err) - } - return agentsdk.ProtoFromServiceBanner(cfg.ServiceBanner), nil -} diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 9e746d2df6abf..3d14f4ec72726 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8272,8 +8272,19 @@ const docTemplate = `{ "logo_url": { "type": "string" }, + "notification_banners": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.BannerConfig" + } + }, "service_banner": { - "$ref": "#/definitions/codersdk.ServiceBannerConfig" + "description": "Deprecated: ServiceBanner has been replaced by NotificationBanners.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.BannerConfig" + } + ] }, "support_links": { "type": "array", @@ -8530,6 +8541,20 @@ const docTemplate = `{ "AutomaticUpdatesNever" ] }, + "codersdk.BannerConfig": { + "type": "object", + "properties": { + "background_color": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "message": { + "type": "string" + } + } + }, "codersdk.BuildInfoResponse": { "type": "object", "properties": { @@ -11060,20 +11085,6 @@ const docTemplate = `{ } } }, - "codersdk.ServiceBannerConfig": { - "type": "object", - "properties": { - "background_color": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "message": { - "type": "string" - } - } - }, "codersdk.SessionCountDeploymentStats": { "type": "object", "properties": { @@ -11906,8 +11917,19 @@ const docTemplate = `{ "logo_url": { "type": "string" }, + "notification_banners": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.BannerConfig" + } + }, "service_banner": { - "$ref": "#/definitions/codersdk.ServiceBannerConfig" + "description": "Deprecated: ServiceBanner has been replaced by NotificationBanners.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.BannerConfig" + } + ] } } }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index d0e60d65aabfe..9f6a1833e995d 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7341,8 +7341,19 @@ "logo_url": { "type": "string" }, + "notification_banners": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.BannerConfig" + } + }, "service_banner": { - "$ref": "#/definitions/codersdk.ServiceBannerConfig" + "description": "Deprecated: ServiceBanner has been replaced by NotificationBanners.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.BannerConfig" + } + ] }, "support_links": { "type": "array", @@ -7588,6 +7599,20 @@ "enum": ["always", "never"], "x-enum-varnames": ["AutomaticUpdatesAlways", "AutomaticUpdatesNever"] }, + "codersdk.BannerConfig": { + "type": "object", + "properties": { + "background_color": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "message": { + "type": "string" + } + } + }, "codersdk.BuildInfoResponse": { "type": "object", "properties": { @@ -9960,20 +9985,6 @@ } } }, - "codersdk.ServiceBannerConfig": { - "type": "object", - "properties": { - "background_color": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "message": { - "type": "string" - } - } - }, "codersdk.SessionCountDeploymentStats": { "type": "object", "properties": { @@ -10763,8 +10774,19 @@ "logo_url": { "type": "string" }, + "notification_banners": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.BannerConfig" + } + }, "service_banner": { - "$ref": "#/definitions/codersdk.ServiceBannerConfig" + "description": "Deprecated: ServiceBanner has been replaced by NotificationBanners.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.BannerConfig" + } + ] } } }, diff --git a/coderd/appearance/appearance.go b/coderd/appearance/appearance.go index 1ac61dea21fe3..f9809036ec84b 100644 --- a/coderd/appearance/appearance.go +++ b/coderd/appearance/appearance.go @@ -32,7 +32,8 @@ type AGPLFetcher struct{} func (AGPLFetcher) Fetch(context.Context) (codersdk.AppearanceConfig, error) { return codersdk.AppearanceConfig{ - SupportLinks: DefaultSupportLinks, + NotificationBanners: []codersdk.BannerConfig{}, + SupportLinks: DefaultSupportLinks, }, nil } diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index a638b705a54f0..aaf623c7a70b5 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1220,6 +1220,11 @@ func (q *querier) GetLogoURL(ctx context.Context) (string, error) { return q.db.GetLogoURL(ctx) } +func (q *querier) GetNotificationBanners(ctx context.Context) (string, error) { + // No authz checks + return q.db.GetNotificationBanners(ctx) +} + func (q *querier) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) { if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceOAuth2ProviderApp); err != nil { return database.OAuth2ProviderApp{}, err @@ -1454,11 +1459,6 @@ func (q *querier) GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Ti return q.db.GetReplicasUpdatedAfter(ctx, updatedAt) } -func (q *querier) GetServiceBanner(ctx context.Context) (string, error) { - // No authz checks - return q.db.GetServiceBanner(ctx) -} - func (q *querier) GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]database.TailnetAgent, error) { if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTailnetCoordinator); err != nil { return nil, err @@ -3364,6 +3364,13 @@ func (q *querier) UpsertLogoURL(ctx context.Context, value string) error { return q.db.UpsertLogoURL(ctx, value) } +func (q *querier) UpsertNotificationBanners(ctx context.Context, value string) error { + if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceDeploymentValues); err != nil { + return err + } + return q.db.UpsertNotificationBanners(ctx, value) +} + func (q *querier) UpsertOAuthSigningKey(ctx context.Context, value string) error { if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceSystem); err != nil { return err @@ -3382,13 +3389,6 @@ func (q *querier) UpsertProvisionerDaemon(ctx context.Context, arg database.Upse return q.db.UpsertProvisionerDaemon(ctx, arg) } -func (q *querier) UpsertServiceBanner(ctx context.Context, value string) error { - if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceDeploymentValues); err != nil { - return err - } - return q.db.UpsertServiceBanner(ctx, value) -} - func (q *querier) UpsertTailnetAgent(ctx context.Context, arg database.UpsertTailnetAgentParams) (database.TailnetAgent, error) { if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTailnetCoordinator); err != nil { return database.TailnetAgent{}, err diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 7be33d58c8dda..48435a0141c64 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -525,7 +525,7 @@ func (s *MethodTestSuite) TestLicense() { s.Run("UpsertLogoURL", s.Subtest(func(db database.Store, check *expects) { check.Args("value").Asserts(rbac.ResourceDeploymentValues, rbac.ActionCreate) })) - s.Run("UpsertServiceBanner", s.Subtest(func(db database.Store, check *expects) { + s.Run("UpsertNotificationBanners", s.Subtest(func(db database.Store, check *expects) { check.Args("value").Asserts(rbac.ResourceDeploymentValues, rbac.ActionCreate) })) s.Run("GetLicenseByID", s.Subtest(func(db database.Store, check *expects) { @@ -556,8 +556,8 @@ func (s *MethodTestSuite) TestLicense() { require.NoError(s.T(), err) check.Args().Asserts().Returns("value") })) - s.Run("GetServiceBanner", s.Subtest(func(db database.Store, check *expects) { - err := db.UpsertServiceBanner(context.Background(), "value") + s.Run("GetNotificationBanners", s.Subtest(func(db database.Store, check *expects) { + err := db.UpsertNotificationBanners(context.Background(), "value") require.NoError(s.T(), err) check.Args().Asserts().Returns("value") })) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index fcc3140133c42..8a2ce25b34367 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -185,7 +185,7 @@ type data struct { deploymentID string derpMeshKey string lastUpdateCheck []byte - serviceBanner []byte + notificationBanners []byte healthSettings []byte applicationName string logoURL string @@ -2488,6 +2488,17 @@ func (q *FakeQuerier) GetLogoURL(_ context.Context) (string, error) { return q.logoURL, nil } +func (q *FakeQuerier) GetNotificationBanners(_ context.Context) (string, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + if q.notificationBanners == nil { + return "", sql.ErrNoRows + } + + return string(q.notificationBanners), nil +} + func (q *FakeQuerier) GetOAuth2ProviderAppByID(_ context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) { q.mutex.Lock() defer q.mutex.Unlock() @@ -3027,17 +3038,6 @@ func (q *FakeQuerier) GetReplicasUpdatedAfter(_ context.Context, updatedAt time. return replicas, nil } -func (q *FakeQuerier) GetServiceBanner(_ context.Context) (string, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - if q.serviceBanner == nil { - return "", sql.ErrNoRows - } - - return string(q.serviceBanner), nil -} - func (*FakeQuerier) GetTailnetAgents(context.Context, uuid.UUID) ([]database.TailnetAgent, error) { return nil, ErrUnimplemented } @@ -8251,6 +8251,14 @@ func (q *FakeQuerier) UpsertLogoURL(_ context.Context, data string) error { return nil } +func (q *FakeQuerier) UpsertNotificationBanners(_ context.Context, data string) error { + q.mutex.RLock() + defer q.mutex.RUnlock() + + q.notificationBanners = []byte(data) + return nil +} + func (q *FakeQuerier) UpsertOAuthSigningKey(_ context.Context, value string) error { q.mutex.Lock() defer q.mutex.Unlock() @@ -8298,14 +8306,6 @@ func (q *FakeQuerier) UpsertProvisionerDaemon(_ context.Context, arg database.Up return d, nil } -func (q *FakeQuerier) UpsertServiceBanner(_ context.Context, data string) error { - q.mutex.RLock() - defer q.mutex.RUnlock() - - q.serviceBanner = []byte(data) - return nil -} - func (*FakeQuerier) UpsertTailnetAgent(context.Context, database.UpsertTailnetAgentParams) (database.TailnetAgent, error) { return database.TailnetAgent{}, ErrUnimplemented } diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 4cb81c1eded86..d92c60e8db09a 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -646,6 +646,13 @@ func (m metricsStore) GetLogoURL(ctx context.Context) (string, error) { return url, err } +func (m metricsStore) GetNotificationBanners(ctx context.Context) (string, error) { + start := time.Now() + r0, r1 := m.s.GetNotificationBanners(ctx) + m.queryLatencies.WithLabelValues("GetNotificationBanners").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m metricsStore) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) { start := time.Now() r0, r1 := m.s.GetOAuth2ProviderAppByID(ctx, id) @@ -849,13 +856,6 @@ func (m metricsStore) GetReplicasUpdatedAfter(ctx context.Context, updatedAt tim return replicas, err } -func (m metricsStore) GetServiceBanner(ctx context.Context) (string, error) { - start := time.Now() - banner, err := m.s.GetServiceBanner(ctx) - m.queryLatencies.WithLabelValues("GetServiceBanner").Observe(time.Since(start).Seconds()) - return banner, err -} - func (m metricsStore) GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]database.TailnetAgent, error) { start := time.Now() r0, r1 := m.s.GetTailnetAgents(ctx, id) @@ -2186,6 +2186,13 @@ func (m metricsStore) UpsertLogoURL(ctx context.Context, value string) error { return r0 } +func (m metricsStore) UpsertNotificationBanners(ctx context.Context, value string) error { + start := time.Now() + r0 := m.s.UpsertNotificationBanners(ctx, value) + m.queryLatencies.WithLabelValues("UpsertNotificationBanners").Observe(time.Since(start).Seconds()) + return r0 +} + func (m metricsStore) UpsertOAuthSigningKey(ctx context.Context, value string) error { start := time.Now() r0 := m.s.UpsertOAuthSigningKey(ctx, value) @@ -2200,13 +2207,6 @@ func (m metricsStore) UpsertProvisionerDaemon(ctx context.Context, arg database. return r0, r1 } -func (m metricsStore) UpsertServiceBanner(ctx context.Context, value string) error { - start := time.Now() - r0 := m.s.UpsertServiceBanner(ctx, value) - m.queryLatencies.WithLabelValues("UpsertServiceBanner").Observe(time.Since(start).Seconds()) - return r0 -} - func (m metricsStore) UpsertTailnetAgent(ctx context.Context, arg database.UpsertTailnetAgentParams) (database.TailnetAgent, error) { start := time.Now() r0, r1 := m.s.UpsertTailnetAgent(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 2bb62e8c92e84..e651c8301c933 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -1275,6 +1275,21 @@ func (mr *MockStoreMockRecorder) GetLogoURL(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogoURL", reflect.TypeOf((*MockStore)(nil).GetLogoURL), arg0) } +// GetNotificationBanners mocks base method. +func (m *MockStore) GetNotificationBanners(arg0 context.Context) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNotificationBanners", arg0) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNotificationBanners indicates an expected call of GetNotificationBanners. +func (mr *MockStoreMockRecorder) GetNotificationBanners(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationBanners", reflect.TypeOf((*MockStore)(nil).GetNotificationBanners), arg0) +} + // GetOAuth2ProviderAppByID mocks base method. func (m *MockStore) GetOAuth2ProviderAppByID(arg0 context.Context, arg1 uuid.UUID) (database.OAuth2ProviderApp, error) { m.ctrl.T.Helper() @@ -1710,21 +1725,6 @@ func (mr *MockStoreMockRecorder) GetReplicasUpdatedAfter(arg0, arg1 any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReplicasUpdatedAfter", reflect.TypeOf((*MockStore)(nil).GetReplicasUpdatedAfter), arg0, arg1) } -// GetServiceBanner mocks base method. -func (m *MockStore) GetServiceBanner(arg0 context.Context) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetServiceBanner", arg0) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetServiceBanner indicates an expected call of GetServiceBanner. -func (mr *MockStoreMockRecorder) GetServiceBanner(arg0 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServiceBanner", reflect.TypeOf((*MockStore)(nil).GetServiceBanner), arg0) -} - // GetTailnetAgents mocks base method. func (m *MockStore) GetTailnetAgents(arg0 context.Context, arg1 uuid.UUID) ([]database.TailnetAgent, error) { m.ctrl.T.Helper() @@ -4577,6 +4577,20 @@ func (mr *MockStoreMockRecorder) UpsertLogoURL(arg0, arg1 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertLogoURL", reflect.TypeOf((*MockStore)(nil).UpsertLogoURL), arg0, arg1) } +// UpsertNotificationBanners mocks base method. +func (m *MockStore) UpsertNotificationBanners(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertNotificationBanners", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpsertNotificationBanners indicates an expected call of UpsertNotificationBanners. +func (mr *MockStoreMockRecorder) UpsertNotificationBanners(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertNotificationBanners", reflect.TypeOf((*MockStore)(nil).UpsertNotificationBanners), arg0, arg1) +} + // UpsertOAuthSigningKey mocks base method. func (m *MockStore) UpsertOAuthSigningKey(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() @@ -4606,20 +4620,6 @@ func (mr *MockStoreMockRecorder) UpsertProvisionerDaemon(arg0, arg1 any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertProvisionerDaemon", reflect.TypeOf((*MockStore)(nil).UpsertProvisionerDaemon), arg0, arg1) } -// UpsertServiceBanner mocks base method. -func (m *MockStore) UpsertServiceBanner(arg0 context.Context, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpsertServiceBanner", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpsertServiceBanner indicates an expected call of UpsertServiceBanner. -func (mr *MockStoreMockRecorder) UpsertServiceBanner(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertServiceBanner", reflect.TypeOf((*MockStore)(nil).UpsertServiceBanner), arg0, arg1) -} - // UpsertTailnetAgent mocks base method. func (m *MockStore) UpsertTailnetAgent(arg0 context.Context, arg1 database.UpsertTailnetAgentParams) (database.TailnetAgent, error) { m.ctrl.T.Helper() diff --git a/coderd/database/migrations/000208_notification_banners.down.sql b/coderd/database/migrations/000208_notification_banners.down.sql new file mode 100644 index 0000000000000..30d149cb016b6 --- /dev/null +++ b/coderd/database/migrations/000208_notification_banners.down.sql @@ -0,0 +1 @@ +delete from site_configs where key = 'notification_banners'; diff --git a/coderd/database/migrations/000208_notification_banners.up.sql b/coderd/database/migrations/000208_notification_banners.up.sql new file mode 100644 index 0000000000000..8f846b16dd509 --- /dev/null +++ b/coderd/database/migrations/000208_notification_banners.up.sql @@ -0,0 +1,4 @@ +update site_configs SET + key = 'notification_banners', + value = concat('[', value, ']') +where key = 'service_banner'; diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 7d8f504cb50e7..405f86bf47688 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -135,6 +135,7 @@ type sqlcQuerier interface { GetLicenseByID(ctx context.Context, id int32) (License, error) GetLicenses(ctx context.Context) ([]License, error) GetLogoURL(ctx context.Context) (string, error) + GetNotificationBanners(ctx context.Context) (string, error) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderApp, error) GetOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppCode, error) GetOAuth2ProviderAppCodeByPrefix(ctx context.Context, secretPrefix []byte) (OAuth2ProviderAppCode, error) @@ -164,7 +165,6 @@ type sqlcQuerier interface { GetQuotaConsumedForUser(ctx context.Context, ownerID uuid.UUID) (int64, error) GetReplicaByID(ctx context.Context, id uuid.UUID) (Replica, error) GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]Replica, error) - GetServiceBanner(ctx context.Context) (string, error) GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]TailnetAgent, error) GetTailnetClientsForAgent(ctx context.Context, agentID uuid.UUID) ([]TailnetClient, error) GetTailnetPeers(ctx context.Context, id uuid.UUID) ([]TailnetPeer, error) @@ -421,9 +421,9 @@ type sqlcQuerier interface { UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg UpsertJFrogXrayScanByWorkspaceAndAgentIDParams) error UpsertLastUpdateCheck(ctx context.Context, value string) error UpsertLogoURL(ctx context.Context, value string) error + UpsertNotificationBanners(ctx context.Context, value string) error UpsertOAuthSigningKey(ctx context.Context, value string) error UpsertProvisionerDaemon(ctx context.Context, arg UpsertProvisionerDaemonParams) (ProvisionerDaemon, error) - UpsertServiceBanner(ctx context.Context, value string) error UpsertTailnetAgent(ctx context.Context, arg UpsertTailnetAgentParams) (TailnetAgent, error) UpsertTailnetClient(ctx context.Context, arg UpsertTailnetClientParams) (TailnetClient, error) UpsertTailnetClientSubscription(ctx context.Context, arg UpsertTailnetClientSubscriptionParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 41171a7473cab..e0fba2dad35bd 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5615,23 +5615,23 @@ func (q *sqlQuerier) GetLogoURL(ctx context.Context) (string, error) { return value, err } -const getOAuthSigningKey = `-- name: GetOAuthSigningKey :one -SELECT value FROM site_configs WHERE key = 'oauth_signing_key' +const getNotificationBanners = `-- name: GetNotificationBanners :one +SELECT value FROM site_configs WHERE key = 'notification_banners' ` -func (q *sqlQuerier) GetOAuthSigningKey(ctx context.Context) (string, error) { - row := q.db.QueryRowContext(ctx, getOAuthSigningKey) +func (q *sqlQuerier) GetNotificationBanners(ctx context.Context) (string, error) { + row := q.db.QueryRowContext(ctx, getNotificationBanners) var value string err := row.Scan(&value) return value, err } -const getServiceBanner = `-- name: GetServiceBanner :one -SELECT value FROM site_configs WHERE key = 'service_banner' +const getOAuthSigningKey = `-- name: GetOAuthSigningKey :one +SELECT value FROM site_configs WHERE key = 'oauth_signing_key' ` -func (q *sqlQuerier) GetServiceBanner(ctx context.Context) (string, error) { - row := q.db.QueryRowContext(ctx, getServiceBanner) +func (q *sqlQuerier) GetOAuthSigningKey(ctx context.Context) (string, error) { + row := q.db.QueryRowContext(ctx, getOAuthSigningKey) var value string err := row.Scan(&value) return value, err @@ -5728,23 +5728,23 @@ func (q *sqlQuerier) UpsertLogoURL(ctx context.Context, value string) error { return err } -const upsertOAuthSigningKey = `-- name: UpsertOAuthSigningKey :exec -INSERT INTO site_configs (key, value) VALUES ('oauth_signing_key', $1) -ON CONFLICT (key) DO UPDATE set value = $1 WHERE site_configs.key = 'oauth_signing_key' +const upsertNotificationBanners = `-- name: UpsertNotificationBanners :exec +INSERT INTO site_configs (key, value) VALUES ('notification_banners', $1) +ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'notification_banners' ` -func (q *sqlQuerier) UpsertOAuthSigningKey(ctx context.Context, value string) error { - _, err := q.db.ExecContext(ctx, upsertOAuthSigningKey, value) +func (q *sqlQuerier) UpsertNotificationBanners(ctx context.Context, value string) error { + _, err := q.db.ExecContext(ctx, upsertNotificationBanners, value) return err } -const upsertServiceBanner = `-- name: UpsertServiceBanner :exec -INSERT INTO site_configs (key, value) VALUES ('service_banner', $1) -ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'service_banner' +const upsertOAuthSigningKey = `-- name: UpsertOAuthSigningKey :exec +INSERT INTO site_configs (key, value) VALUES ('oauth_signing_key', $1) +ON CONFLICT (key) DO UPDATE set value = $1 WHERE site_configs.key = 'oauth_signing_key' ` -func (q *sqlQuerier) UpsertServiceBanner(ctx context.Context, value string) error { - _, err := q.db.ExecContext(ctx, upsertServiceBanner, value) +func (q *sqlQuerier) UpsertOAuthSigningKey(ctx context.Context, value string) error { + _, err := q.db.ExecContext(ctx, upsertOAuthSigningKey, value) return err } diff --git a/coderd/database/queries/siteconfig.sql b/coderd/database/queries/siteconfig.sql index a432b71e3a91d..b827c6e19e959 100644 --- a/coderd/database/queries/siteconfig.sql +++ b/coderd/database/queries/siteconfig.sql @@ -36,12 +36,12 @@ ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'last_update -- name: GetLastUpdateCheck :one SELECT value FROM site_configs WHERE key = 'last_update_check'; --- name: UpsertServiceBanner :exec -INSERT INTO site_configs (key, value) VALUES ('service_banner', $1) -ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'service_banner'; +-- name: UpsertNotificationBanners :exec +INSERT INTO site_configs (key, value) VALUES ('notification_banners', $1) +ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'notification_banners'; --- name: GetServiceBanner :one -SELECT value FROM site_configs WHERE key = 'service_banner'; +-- name: GetNotificationBanners :one +SELECT value FROM site_configs WHERE key = 'notification_banners'; -- name: UpsertLogoURL :exec INSERT INTO site_configs (key, value) VALUES ('logo_url', $1) diff --git a/codersdk/agentsdk/convert.go b/codersdk/agentsdk/convert.go index 8671d9e0b51e3..adfabd1510768 100644 --- a/codersdk/agentsdk/convert.go +++ b/codersdk/agentsdk/convert.go @@ -277,15 +277,15 @@ func ProtoFromApp(a codersdk.WorkspaceApp) (*proto.WorkspaceApp, error) { }, nil } -func ServiceBannerFromProto(sbp *proto.ServiceBanner) codersdk.ServiceBannerConfig { - return codersdk.ServiceBannerConfig{ +func ServiceBannerFromProto(sbp *proto.ServiceBanner) codersdk.BannerConfig { + return codersdk.BannerConfig{ Enabled: sbp.GetEnabled(), Message: sbp.GetMessage(), BackgroundColor: sbp.GetBackgroundColor(), } } -func ProtoFromServiceBanner(sb codersdk.ServiceBannerConfig) *proto.ServiceBanner { +func ProtoFromServiceBanner(sb codersdk.BannerConfig) *proto.ServiceBanner { return &proto.ServiceBanner{ Enabled: sb.Enabled, Message: sb.Message, @@ -293,6 +293,22 @@ func ProtoFromServiceBanner(sb codersdk.ServiceBannerConfig) *proto.ServiceBanne } } +func BannerConfigFromProto(sbp *proto.BannerConfig) codersdk.BannerConfig { + return codersdk.BannerConfig{ + Enabled: sbp.GetEnabled(), + Message: sbp.GetMessage(), + BackgroundColor: sbp.GetBackgroundColor(), + } +} + +func ProtoFromBannerConfig(sb codersdk.BannerConfig) *proto.BannerConfig { + return &proto.BannerConfig{ + Enabled: sb.Enabled, + Message: sb.Message, + BackgroundColor: sb.BackgroundColor, + } +} + func ProtoFromSubsystems(ss []codersdk.AgentSubsystem) ([]proto.Startup_Subsystem, error) { ret := make([]proto.Startup_Subsystem, len(ss)) for i, s := range ss { diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 2aa675727b72b..087ad660cbc68 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -2100,19 +2100,26 @@ func (c *Client) DeploymentStats(ctx context.Context) (DeploymentStats, error) { } type AppearanceConfig struct { - ApplicationName string `json:"application_name"` - LogoURL string `json:"logo_url"` - ServiceBanner ServiceBannerConfig `json:"service_banner"` - SupportLinks []LinkConfig `json:"support_links,omitempty"` + ApplicationName string `json:"application_name"` + LogoURL string `json:"logo_url"` + // Deprecated: ServiceBanner has been replaced by NotificationBanners. + ServiceBanner BannerConfig `json:"service_banner"` + NotificationBanners []BannerConfig `json:"notification_banners"` + SupportLinks []LinkConfig `json:"support_links,omitempty"` } type UpdateAppearanceConfig struct { - ApplicationName string `json:"application_name"` - LogoURL string `json:"logo_url"` - ServiceBanner ServiceBannerConfig `json:"service_banner"` + ApplicationName string `json:"application_name"` + LogoURL string `json:"logo_url"` + // Deprecated: ServiceBanner has been replaced by NotificationBanners. + ServiceBanner BannerConfig `json:"service_banner"` + NotificationBanners []BannerConfig `json:"notification_banners"` } -type ServiceBannerConfig struct { +// Deprecated: ServiceBannerConfig has been renamed to BannerConfig. +type ServiceBannerConfig = BannerConfig + +type BannerConfig struct { Enabled bool `json:"enabled"` Message string `json:"message,omitempty"` BackgroundColor string `json:"background_color,omitempty"` diff --git a/codersdk/organizations.go b/codersdk/organizations.go index f887d5ea4de5a..441f4774f2441 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -34,7 +34,7 @@ func ProvisionerTypeValid[T ProvisionerType | string](pt T) error { case string(ProvisionerTypeEcho), string(ProvisionerTypeTerraform): return nil default: - return fmt.Errorf("provisioner type '%s' is not supported", pt) + return xerrors.Errorf("provisioner type '%s' is not supported", pt) } } diff --git a/docs/api/enterprise.md b/docs/api/enterprise.md index 0b05a9fffeee6..800e9e517196d 100644 --- a/docs/api/enterprise.md +++ b/docs/api/enterprise.md @@ -21,6 +21,13 @@ curl -X GET http://coder-server:8080/api/v2/appearance \ { "application_name": "string", "logo_url": "string", + "notification_banners": [ + { + "background_color": "string", + "enabled": true, + "message": "string" + } + ], "service_banner": { "background_color": "string", "enabled": true, @@ -64,6 +71,13 @@ curl -X PUT http://coder-server:8080/api/v2/appearance \ { "application_name": "string", "logo_url": "string", + "notification_banners": [ + { + "background_color": "string", + "enabled": true, + "message": "string" + } + ], "service_banner": { "background_color": "string", "enabled": true, @@ -86,6 +100,13 @@ curl -X PUT http://coder-server:8080/api/v2/appearance \ { "application_name": "string", "logo_url": "string", + "notification_banners": [ + { + "background_color": "string", + "enabled": true, + "message": "string" + } + ], "service_banner": { "background_color": "string", "enabled": true, diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 26b38a7c1ec78..a6462a14ca29c 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -751,6 +751,13 @@ { "application_name": "string", "logo_url": "string", + "notification_banners": [ + { + "background_color": "string", + "enabled": true, + "message": "string" + } + ], "service_banner": { "background_color": "string", "enabled": true, @@ -768,12 +775,13 @@ ### Properties -| Name | Type | Required | Restrictions | Description | -| ------------------ | ------------------------------------------------------------ | -------- | ------------ | ----------- | -| `application_name` | string | false | | | -| `logo_url` | string | false | | | -| `service_banner` | [codersdk.ServiceBannerConfig](#codersdkservicebannerconfig) | false | | | -| `support_links` | array of [codersdk.LinkConfig](#codersdklinkconfig) | false | | | +| Name | Type | Required | Restrictions | Description | +| ---------------------- | ------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------- | +| `application_name` | string | false | | | +| `logo_url` | string | false | | | +| `notification_banners` | array of [codersdk.BannerConfig](#codersdkbannerconfig) | false | | | +| `service_banner` | [codersdk.BannerConfig](#codersdkbannerconfig) | false | | Deprecated: ServiceBanner has been replaced by NotificationBanners. | +| `support_links` | array of [codersdk.LinkConfig](#codersdklinkconfig) | false | | | ## codersdk.ArchiveTemplateVersionsRequest @@ -1172,6 +1180,24 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `always` | | `never` | +## codersdk.BannerConfig + +```json +{ + "background_color": "string", + "enabled": true, + "message": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------------------ | ------- | -------- | ------------ | ----------- | +| `background_color` | string | false | | | +| `enabled` | boolean | false | | | +| `message` | string | false | | | + ## codersdk.BuildInfoResponse ```json @@ -4264,24 +4290,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `ssh_config_options` | object | false | | | | » `[any property]` | string | false | | | -## codersdk.ServiceBannerConfig - -```json -{ - "background_color": "string", - "enabled": true, - "message": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ------------------ | ------- | -------- | ------------ | ----------- | -| `background_color` | string | false | | | -| `enabled` | boolean | false | | | -| `message` | string | false | | | - ## codersdk.SessionCountDeploymentStats ```json @@ -5174,6 +5182,13 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o { "application_name": "string", "logo_url": "string", + "notification_banners": [ + { + "background_color": "string", + "enabled": true, + "message": "string" + } + ], "service_banner": { "background_color": "string", "enabled": true, @@ -5184,11 +5199,12 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o ### Properties -| Name | Type | Required | Restrictions | Description | -| ------------------ | ------------------------------------------------------------ | -------- | ------------ | ----------- | -| `application_name` | string | false | | | -| `logo_url` | string | false | | | -| `service_banner` | [codersdk.ServiceBannerConfig](#codersdkservicebannerconfig) | false | | | +| Name | Type | Required | Restrictions | Description | +| ---------------------- | ------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------- | +| `application_name` | string | false | | | +| `logo_url` | string | false | | | +| `notification_banners` | array of [codersdk.BannerConfig](#codersdkbannerconfig) | false | | | +| `service_banner` | [codersdk.BannerConfig](#codersdkbannerconfig) | false | | Deprecated: ServiceBanner has been replaced by NotificationBanners. | ## codersdk.UpdateCheckResponse diff --git a/enterprise/coderd/appearance.go b/enterprise/coderd/appearance.go index 70ef238d6056c..7029340672b6e 100644 --- a/enterprise/coderd/appearance.go +++ b/enterprise/coderd/appearance.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "encoding/json" "errors" + "fmt" "net/http" "golang.org/x/sync/errgroup" @@ -53,9 +54,11 @@ func newAppearanceFetcher(store database.Store, links []codersdk.LinkConfig) agp func (f *appearanceFetcher) Fetch(ctx context.Context) (codersdk.AppearanceConfig, error) { var eg errgroup.Group - var applicationName string - var logoURL string - var serviceBannerJSON string + var ( + applicationName string + logoURL string + notificationBannersJSON string + ) eg.Go(func() (err error) { applicationName, err = f.database.GetApplicationName(ctx) if err != nil && !errors.Is(err, sql.ErrNoRows) { @@ -71,9 +74,9 @@ func (f *appearanceFetcher) Fetch(ctx context.Context) (codersdk.AppearanceConfi return nil }) eg.Go(func() (err error) { - serviceBannerJSON, err = f.database.GetServiceBanner(ctx) + notificationBannersJSON, err = f.database.GetNotificationBanners(ctx) if err != nil && !errors.Is(err, sql.ErrNoRows) { - return xerrors.Errorf("get service banner: %w", err) + return xerrors.Errorf("get notification banners: %w", err) } return nil }) @@ -83,21 +86,27 @@ func (f *appearanceFetcher) Fetch(ctx context.Context) (codersdk.AppearanceConfi } cfg := codersdk.AppearanceConfig{ - ApplicationName: applicationName, - LogoURL: logoURL, + ApplicationName: applicationName, + LogoURL: logoURL, + NotificationBanners: []codersdk.BannerConfig{}, + SupportLinks: agpl.DefaultSupportLinks, } - if serviceBannerJSON != "" { - err = json.Unmarshal([]byte(serviceBannerJSON), &cfg.ServiceBanner) + + if notificationBannersJSON != "" { + err = json.Unmarshal([]byte(notificationBannersJSON), &cfg.NotificationBanners) if err != nil { return codersdk.AppearanceConfig{}, xerrors.Errorf( - "unmarshal json: %w, raw: %s", err, serviceBannerJSON, + "unmarshal notification banners json: %w, raw: %s", err, notificationBannersJSON, ) } - } - if len(f.supportLinks) == 0 { - cfg.SupportLinks = agpl.DefaultSupportLinks - } else { + // Redundant, but improves compatibility with slightly mismatched agent versions. + // Maybe we can remove this after a grace period? -Kayla, May 6th 2024 + if len(cfg.NotificationBanners) > 0 { + cfg.ServiceBanner = cfg.NotificationBanners[0] + } + } + if len(f.supportLinks) > 0 { cfg.SupportLinks = f.supportLinks } @@ -139,29 +148,32 @@ func (api *API) putAppearance(rw http.ResponseWriter, r *http.Request) { return } - if appearance.ServiceBanner.Enabled { - if err := validateHexColor(appearance.ServiceBanner.BackgroundColor); err != nil { + for _, banner := range appearance.NotificationBanners { + if err := validateHexColor(banner.BackgroundColor); err != nil { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Invalid color format", + Message: fmt.Sprintf("Invalid color format: %q", banner.BackgroundColor), Detail: err.Error(), }) return } } - serviceBannerJSON, err := json.Marshal(appearance.ServiceBanner) + if appearance.NotificationBanners == nil { + appearance.NotificationBanners = []codersdk.BannerConfig{} + } + notificationBannersJSON, err := json.Marshal(appearance.NotificationBanners) if err != nil { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Unable to marshal service banner", + Message: "Unable to marshal notification banners", Detail: err.Error(), }) return } - err = api.Database.UpsertServiceBanner(ctx, string(serviceBannerJSON)) + err = api.Database.UpsertNotificationBanners(ctx, string(notificationBannersJSON)) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Unable to set service banner", + Message: "Unable to set notification banners", Detail: err.Error(), }) return diff --git a/enterprise/coderd/appearance_test.go b/enterprise/coderd/appearance_test.go index beab7c104f5e0..745f90e00d03b 100644 --- a/enterprise/coderd/appearance_test.go +++ b/enterprise/coderd/appearance_test.go @@ -6,7 +6,6 @@ import ( "net/http" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/agent/proto" @@ -56,7 +55,7 @@ func TestCustomLogoAndCompanyName(t *testing.T) { require.Equal(t, uac.LogoURL, got.LogoURL) } -func TestServiceBanners(t *testing.T) { +func TestNotificationBanners(t *testing.T) { t.Parallel() t.Run("User", func(t *testing.T) { @@ -68,10 +67,10 @@ func TestServiceBanners(t *testing.T) { adminClient, adminUser := coderdenttest.New(t, &coderdenttest.Options{DontAddLicense: true}) basicUserClient, _ := coderdtest.CreateAnotherUser(t, adminClient, adminUser.OrganizationID) - // Even without a license, the banner should return as disabled. + // Without a license, there should be no banners. sb, err := basicUserClient.Appearance(ctx) require.NoError(t, err) - require.False(t, sb.ServiceBanner.Enabled) + require.Empty(t, sb.NotificationBanners) coderdenttest.AddLicense(t, adminClient, coderdenttest.LicenseOptions{ Features: license.Features{ @@ -82,43 +81,42 @@ func TestServiceBanners(t *testing.T) { // Default state sb, err = basicUserClient.Appearance(ctx) require.NoError(t, err) - require.False(t, sb.ServiceBanner.Enabled) + require.Empty(t, sb.NotificationBanners) + // Regular user should be unable to set the banner uac := codersdk.UpdateAppearanceConfig{ - ServiceBanner: sb.ServiceBanner, + NotificationBanners: []codersdk.BannerConfig{{Enabled: true}}, } - // Regular user should be unable to set the banner - uac.ServiceBanner.Enabled = true - err = basicUserClient.UpdateAppearance(ctx, uac) require.Error(t, err) var sdkError *codersdk.Error require.True(t, errors.As(err, &sdkError)) + require.ErrorAs(t, err, &sdkError) require.Equal(t, http.StatusForbidden, sdkError.StatusCode()) // But an admin can - wantBanner := uac - wantBanner.ServiceBanner.Enabled = true - wantBanner.ServiceBanner.Message = "Hey" - wantBanner.ServiceBanner.BackgroundColor = "#00FF00" + wantBanner := codersdk.UpdateAppearanceConfig{ + NotificationBanners: []codersdk.BannerConfig{{ + Enabled: true, + Message: "The beep-bop will be boop-beeped on Saturday at 12AM PST.", + BackgroundColor: "#00FF00", + }}, + } err = adminClient.UpdateAppearance(ctx, wantBanner) require.NoError(t, err) gotBanner, err := adminClient.Appearance(ctx) //nolint:gocritic // we should assert at least once that the owner can get the banner require.NoError(t, err) - gotBanner.SupportLinks = nil // clean "support links" before comparison - require.Equal(t, wantBanner.ServiceBanner, gotBanner.ServiceBanner) + require.Equal(t, wantBanner.NotificationBanners, gotBanner.NotificationBanners) // But even an admin can't give a bad color - wantBanner.ServiceBanner.BackgroundColor = "#bad color" + wantBanner.NotificationBanners[0].BackgroundColor = "#bad color" err = adminClient.UpdateAppearance(ctx, wantBanner) require.Error(t, err) - var sdkErr *codersdk.Error - if assert.ErrorAs(t, err, &sdkErr) { - assert.Equal(t, http.StatusBadRequest, sdkErr.StatusCode()) - assert.Contains(t, sdkErr.Message, "Invalid color format") - assert.Contains(t, sdkErr.Detail, "expected # prefix and 6 characters") - } + require.ErrorAs(t, err, &sdkErr) + require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode()) + require.Contains(t, sdkErr.Message, "Invalid color format") + require.Contains(t, sdkErr.Detail, "expected # prefix and 6 characters") }) t.Run("Agent", func(t *testing.T) { @@ -141,11 +139,11 @@ func TestServiceBanners(t *testing.T) { }, }) cfg := codersdk.UpdateAppearanceConfig{ - ServiceBanner: codersdk.ServiceBannerConfig{ + NotificationBanners: []codersdk.BannerConfig{{ Enabled: true, - Message: "Hey", + Message: "The beep-bop will be boop-beeped on Saturday at 12AM PST.", BackgroundColor: "#00FF00", - }, + }}, } err := client.UpdateAppearance(ctx, cfg) require.NoError(t, err) @@ -157,34 +155,38 @@ func TestServiceBanners(t *testing.T) { agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(r.AgentToken) - banner := requireGetServiceBanner(ctx, t, agentClient) - require.Equal(t, cfg.ServiceBanner, banner) + banners := requireGetNotificationBanners(ctx, t, agentClient) + require.Equal(t, cfg.NotificationBanners, banners) // Create an AGPL Coderd against the same database agplClient := coderdtest.New(t, &coderdtest.Options{Database: store, Pubsub: ps}) agplAgentClient := agentsdk.New(agplClient.URL) agplAgentClient.SetSessionToken(r.AgentToken) - banner = requireGetServiceBanner(ctx, t, agplAgentClient) - require.Equal(t, codersdk.ServiceBannerConfig{}, banner) + banners = requireGetNotificationBanners(ctx, t, agplAgentClient) + require.Equal(t, []codersdk.BannerConfig{}, banners) // No license means no banner. err = client.DeleteLicense(ctx, lic.ID) require.NoError(t, err) - banner = requireGetServiceBanner(ctx, t, agentClient) - require.Equal(t, codersdk.ServiceBannerConfig{}, banner) + banners = requireGetNotificationBanners(ctx, t, agentClient) + require.Equal(t, []codersdk.BannerConfig{}, banners) }) } -func requireGetServiceBanner(ctx context.Context, t *testing.T, client *agentsdk.Client) codersdk.ServiceBannerConfig { +func requireGetNotificationBanners(ctx context.Context, t *testing.T, client *agentsdk.Client) []codersdk.BannerConfig { cc, err := client.ConnectRPC(ctx) require.NoError(t, err) defer func() { _ = cc.Close() }() aAPI := proto.NewDRPCAgentClient(cc) - sbp, err := aAPI.GetServiceBanner(ctx, &proto.GetServiceBannerRequest{}) + bannersProto, err := aAPI.GetNotificationBanners(ctx, &proto.GetNotificationBannersRequest{}) require.NoError(t, err) - return agentsdk.ServiceBannerFromProto(sbp) + banners := make([]codersdk.BannerConfig, 0, len(bannersProto.NotificationBanners)) + for _, bannerProto := range bannersProto.NotificationBanners { + banners = append(banners, agentsdk.BannerConfigFromProto(bannerProto)) + } + return banners } func TestCustomSupportLinks(t *testing.T) { diff --git a/provisionerd/provisionerd_test.go b/provisionerd/provisionerd_test.go index bca072707f491..b3cf08fc1ca5a 100644 --- a/provisionerd/provisionerd_test.go +++ b/provisionerd/provisionerd_test.go @@ -611,7 +611,7 @@ func TestProvisionerd(t *testing.T) { server := createProvisionerd(t, func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) { // This is the dial out to Coderd, which in this unit test will always fail. connectAttemptedClose.Do(func() { close(connectAttempted) }) - return nil, fmt.Errorf("client connection always fails") + return nil, xerrors.New("client connection always fails") }, provisionerd.LocalProvisioners{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{}), }) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 6b4102073b3d1..c677ffbcb1b3b 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1357,6 +1357,7 @@ export const getAppearance = async (): Promise<TypesGen.AppearanceConfig> => { service_banner: { enabled: false, }, + notification_banners: [], }; } throw ex; diff --git a/site/src/api/queries/appearance.ts b/site/src/api/queries/appearance.ts index d9337bc39e79d..7fc6cd1a71b9d 100644 --- a/site/src/api/queries/appearance.ts +++ b/site/src/api/queries/appearance.ts @@ -4,12 +4,12 @@ import type { AppearanceConfig } from "api/typesGenerated"; import type { MetadataState } from "hooks/useEmbeddedMetadata"; import { cachedQuery } from "./util"; -const appearanceConfigKey = ["appearance"] as const; +export const appearanceConfigKey = ["appearance"] as const; export const appearance = (metadata: MetadataState<AppearanceConfig>) => { return cachedQuery({ metadata, - queryKey: ["appearance"], + queryKey: appearanceConfigKey, queryFn: () => API.getAppearance(), }); }; diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index d91a6b430de27..c2e9b51b96a11 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -48,7 +48,8 @@ export interface AppHostResponse { export interface AppearanceConfig { readonly application_name: string; readonly logo_url: string; - readonly service_banner: ServiceBannerConfig; + readonly service_banner: BannerConfig; + readonly notification_banners: readonly BannerConfig[]; readonly support_links?: readonly LinkConfig[]; } @@ -157,6 +158,13 @@ export interface AvailableExperiments { readonly safe: readonly Experiment[]; } +// From codersdk/deployment.go +export interface BannerConfig { + readonly enabled: boolean; + readonly message?: string; + readonly background_color?: string; +} + // From codersdk/deployment.go export interface BuildInfoResponse { readonly external_url: string; @@ -1281,7 +1289,8 @@ export interface UpdateActiveTemplateVersion { export interface UpdateAppearanceConfig { readonly application_name: string; readonly logo_url: string; - readonly service_banner: ServiceBannerConfig; + readonly service_banner: BannerConfig; + readonly notification_banners: readonly BannerConfig[]; } // From codersdk/updatecheck.go diff --git a/site/src/modules/dashboard/DashboardLayout.tsx b/site/src/modules/dashboard/DashboardLayout.tsx index d698e77f001ca..2a1f545820115 100644 --- a/site/src/modules/dashboard/DashboardLayout.tsx +++ b/site/src/modules/dashboard/DashboardLayout.tsx @@ -7,7 +7,7 @@ import { Outlet } from "react-router-dom"; import { Loader } from "components/Loader/Loader"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { LicenseBanner } from "modules/dashboard/LicenseBanner/LicenseBanner"; -import { ServiceBanner } from "modules/dashboard/ServiceBanner/ServiceBanner"; +import { NotificationBanners } from "modules/dashboard/NotificationBanners/NotificationBanners"; import { dashboardContentBottomPadding } from "theme/constants"; import { docs } from "utils/docs"; import { DeploymentBanner } from "./DeploymentBanner/DeploymentBanner"; @@ -21,8 +21,8 @@ export const DashboardLayout: FC = () => { return ( <> - <ServiceBanner /> {canViewDeployment && <LicenseBanner />} + <NotificationBanners /> <div css={{ diff --git a/site/src/modules/dashboard/DashboardProvider.tsx b/site/src/modules/dashboard/DashboardProvider.tsx index 3cb85c2461c8d..19daf886f02f8 100644 --- a/site/src/modules/dashboard/DashboardProvider.tsx +++ b/site/src/modules/dashboard/DashboardProvider.tsx @@ -1,10 +1,4 @@ -import { - createContext, - type FC, - type PropsWithChildren, - useCallback, - useState, -} from "react"; +import { createContext, type FC, type PropsWithChildren } from "react"; import { useQuery } from "react-query"; import { appearance } from "api/queries/appearance"; import { entitlements } from "api/queries/entitlements"; @@ -14,21 +8,13 @@ import type { Entitlements, Experiments, } from "api/typesGenerated"; -import { displayError } from "components/GlobalSnackbar/utils"; import { Loader } from "components/Loader/Loader"; import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; -import { hslToHex, isHexColor, isHslColor } from "utils/colors"; - -interface Appearance { - config: AppearanceConfig; - isPreview: boolean; - setPreview: (config: AppearanceConfig) => void; -} export interface DashboardValue { entitlements: Entitlements; experiments: Experiments; - appearance: Appearance; + appearance: AppearanceConfig; } export const DashboardContext = createContext<DashboardValue | undefined>( @@ -44,34 +30,6 @@ export const DashboardProvider: FC<PropsWithChildren> = ({ children }) => { const isLoading = !entitlementsQuery.data || !appearanceQuery.data || !experimentsQuery.data; - const [configPreview, setConfigPreview] = useState<AppearanceConfig>(); - - // Centralizing the logic for catching malformed configs in one spot, just to - // be on the safe side; don't want to expose raw setConfigPreview outside - // the provider - const setPreview = useCallback((newConfig: AppearanceConfig) => { - // Have runtime safety nets in place, just because so much of the codebase - // relies on HSL for formatting, but server expects hex values. Can't catch - // color format mismatches at the type level - const incomingBg = newConfig.service_banner.background_color; - let configForDispatch = newConfig; - - if (typeof incomingBg === "string" && isHslColor(incomingBg)) { - configForDispatch = { - ...newConfig, - service_banner: { - ...newConfig.service_banner, - background_color: hslToHex(incomingBg), - }, - }; - } else if (typeof incomingBg === "string" && !isHexColor(incomingBg)) { - displayError(`The value ${incomingBg} is not a valid hex string`); - return; - } - - setConfigPreview(configForDispatch); - }, []); - if (isLoading) { return <Loader fullscreen />; } @@ -81,11 +39,7 @@ export const DashboardProvider: FC<PropsWithChildren> = ({ children }) => { value={{ entitlements: entitlementsQuery.data, experiments: experimentsQuery.data, - appearance: { - config: configPreview ?? appearanceQuery.data, - setPreview: setPreview, - isPreview: configPreview !== undefined, - }, + appearance: appearanceQuery.data, }} > {children} diff --git a/site/src/modules/dashboard/Navbar/Navbar.tsx b/site/src/modules/dashboard/Navbar/Navbar.tsx index 388622fdf7636..8a0b473398a70 100644 --- a/site/src/modules/dashboard/Navbar/Navbar.tsx +++ b/site/src/modules/dashboard/Navbar/Navbar.tsx @@ -25,9 +25,9 @@ export const Navbar: FC = () => { return ( <NavbarView user={me} - logo_url={appearance.config.logo_url} + logo_url={appearance.logo_url} buildInfo={buildInfoQuery.data} - supportLinks={appearance.config.support_links} + supportLinks={appearance.support_links} onSignOut={signOut} canViewAuditLog={canViewAuditLog} canViewDeployment={canViewDeployment} diff --git a/site/src/modules/dashboard/NotificationBanners/NotificationBannerView.stories.tsx b/site/src/modules/dashboard/NotificationBanners/NotificationBannerView.stories.tsx new file mode 100644 index 0000000000000..ee5f8dece47ca --- /dev/null +++ b/site/src/modules/dashboard/NotificationBanners/NotificationBannerView.stories.tsx @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { NotificationBannerView } from "./NotificationBannerView"; + +const meta: Meta<typeof NotificationBannerView> = { + title: "modules/dashboard/NotificationBannerView", + component: NotificationBannerView, +}; + +export default meta; +type Story = StoryObj<typeof NotificationBannerView>; + +export const Production: Story = { + args: { + message: "Unfortunately, there's a radio connected to my brain.", + backgroundColor: "#ffaff3", + }, +}; + +export const Preview: Story = { + args: { + message: "バアン バン バン バン バアン ブレイバアン!", + backgroundColor: "#4cd473", + }, +}; diff --git a/site/src/modules/dashboard/ServiceBanner/ServiceBannerView.tsx b/site/src/modules/dashboard/NotificationBanners/NotificationBannerView.tsx similarity index 57% rename from site/src/modules/dashboard/ServiceBanner/ServiceBannerView.tsx rename to site/src/modules/dashboard/NotificationBanners/NotificationBannerView.tsx index e907085cb2af4..4832ea93f6065 100644 --- a/site/src/modules/dashboard/ServiceBanner/ServiceBannerView.tsx +++ b/site/src/modules/dashboard/NotificationBanners/NotificationBannerView.tsx @@ -1,28 +1,30 @@ import { css, type Interpolation, type Theme } from "@emotion/react"; import type { FC } from "react"; import { InlineMarkdown } from "components/Markdown/Markdown"; -import { Pill } from "components/Pill/Pill"; import { readableForegroundColor } from "utils/colors"; -export interface ServiceBannerViewProps { - message: string; - backgroundColor: string; - isPreview: boolean; +export interface NotificationBannerViewProps { + message?: string; + backgroundColor?: string; } -export const ServiceBannerView: FC<ServiceBannerViewProps> = ({ +export const NotificationBannerView: FC<NotificationBannerViewProps> = ({ message, backgroundColor, - isPreview, }) => { + if (!message || !backgroundColor) { + return null; + } + return ( - <div css={[styles.banner, { backgroundColor }]} className="service-banner"> - {isPreview && <Pill type="info">Preview</Pill>} + <div + css={styles.banner} + style={{ backgroundColor }} + className="service-banner" + > <div - css={[ - styles.wrapper, - { color: readableForegroundColor(backgroundColor) }, - ]} + css={styles.wrapper} + style={{ color: readableForegroundColor(backgroundColor) }} > <InlineMarkdown>{message}</InlineMarkdown> </div> diff --git a/site/src/modules/dashboard/NotificationBanners/NotificationBanners.tsx b/site/src/modules/dashboard/NotificationBanners/NotificationBanners.tsx new file mode 100644 index 0000000000000..a8ab663721a46 --- /dev/null +++ b/site/src/modules/dashboard/NotificationBanners/NotificationBanners.tsx @@ -0,0 +1,28 @@ +import type { FC } from "react"; +import { useDashboard } from "modules/dashboard/useDashboard"; +import { NotificationBannerView } from "./NotificationBannerView"; + +export const NotificationBanners: FC = () => { + const { appearance, entitlements } = useDashboard(); + const notificationBanners = appearance.notification_banners; + + const isEntitled = + entitlements.features.appearance.entitlement !== "not_entitled"; + if (!isEntitled) { + return null; + } + + return ( + <> + {notificationBanners + .filter((banner) => banner.enabled) + .map((banner) => ( + <NotificationBannerView + key={banner.message} + message={banner.message} + backgroundColor={banner.background_color} + /> + ))} + </> + ); +}; diff --git a/site/src/modules/dashboard/ServiceBanner/ServiceBanner.tsx b/site/src/modules/dashboard/ServiceBanner/ServiceBanner.tsx deleted file mode 100644 index cedd4ba4e77f0..0000000000000 --- a/site/src/modules/dashboard/ServiceBanner/ServiceBanner.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { FC } from "react"; -import { useDashboard } from "modules/dashboard/useDashboard"; -import { ServiceBannerView } from "./ServiceBannerView"; - -export const ServiceBanner: FC = () => { - const { appearance } = useDashboard(); - const { message, background_color, enabled } = - appearance.config.service_banner; - - if (!enabled || message === undefined || background_color === undefined) { - return null; - } - - return ( - <ServiceBannerView - message={message} - backgroundColor={background_color} - isPreview={appearance.isPreview} - /> - ); -}; diff --git a/site/src/modules/dashboard/ServiceBanner/ServiceBannerView.stories.tsx b/site/src/modules/dashboard/ServiceBanner/ServiceBannerView.stories.tsx deleted file mode 100644 index 1f3df18b3a42a..0000000000000 --- a/site/src/modules/dashboard/ServiceBanner/ServiceBannerView.stories.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { ServiceBannerView } from "./ServiceBannerView"; - -const meta: Meta<typeof ServiceBannerView> = { - title: "modules/dashboard/ServiceBannerView", - component: ServiceBannerView, -}; - -export default meta; -type Story = StoryObj<typeof ServiceBannerView>; - -export const Production: Story = { - args: { - message: "weeeee", - backgroundColor: "#FFFFFF", - }, -}; - -export const Preview: Story = { - args: { - message: "weeeee", - backgroundColor: "#000000", - isPreview: true, - }, -}; diff --git a/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx b/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx index a9c065c33b330..83da1063488b7 100644 --- a/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx @@ -18,12 +18,6 @@ import { } from "testHelpers/entities"; import { WorkspaceStatusBadge } from "./WorkspaceStatusBadge"; -const MockedAppearance = { - config: MockAppearanceConfig, - isPreview: false, - setPreview: () => {}, -}; - const meta: Meta<typeof WorkspaceStatusBadge> = { title: "modules/workspaces/WorkspaceStatusBadge", component: WorkspaceStatusBadge, @@ -41,7 +35,7 @@ const meta: Meta<typeof WorkspaceStatusBadge> = { value={{ entitlements: MockEntitlementsWithScheduling, experiments: MockExperiments, - appearance: MockedAppearance, + appearance: MockAppearanceConfig, }} > <Story /> diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx index 2f05eacb9a20a..a99e04dd6b8e0 100644 --- a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx +++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx @@ -2,7 +2,7 @@ import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQueryClient } from "react-query"; import { getErrorMessage } from "api/errors"; -import { updateAppearance } from "api/queries/appearance"; +import { appearanceConfigKey, updateAppearance } from "api/queries/appearance"; import type { UpdateAppearanceConfig } from "api/typesGenerated"; import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; import { useDashboard } from "modules/dashboard/useDashboard"; @@ -20,16 +20,12 @@ const AppearanceSettingsPage: FC = () => { const onSaveAppearance = async ( newConfig: Partial<UpdateAppearanceConfig>, - preview: boolean, ) => { - const newAppearance = { ...appearance.config, ...newConfig }; - if (preview) { - appearance.setPreview(newAppearance); - return; - } + const newAppearance = { ...appearance, ...newConfig }; try { await updateAppearanceMutation.mutateAsync(newAppearance); + await queryClient.invalidateQueries(appearanceConfigKey); displaySuccess("Successfully updated appearance settings!"); } catch (error) { displayError( @@ -45,7 +41,7 @@ const AppearanceSettingsPage: FC = () => { </Helmet> <AppearanceSettingsPageView - appearance={appearance.config} + appearance={appearance} onSaveAppearance={onSaveAppearance} isEntitled={ entitlements.features.appearance.entitlement !== "not_entitled" diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx index 60e0581ddc661..ed27e6bddaf96 100644 --- a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx +++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx @@ -9,10 +9,17 @@ const meta: Meta<typeof AppearanceSettingsPageView> = { application_name: "Foobar", logo_url: "https://github.com/coder.png", service_banner: { - enabled: true, - message: "hello world", - background_color: "white", + enabled: false, + message: "", + background_color: "#00ff00", }, + notification_banners: [ + { + enabled: true, + message: "The beep-bop will be boop-beeped on Saturday at 12AM PST.", + background_color: "#ffaff3", + }, + ], }, isEntitled: false, }, diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx index 784ccb94ac3b3..b62a20e923c89 100644 --- a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx +++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx @@ -1,13 +1,8 @@ -import { useTheme } from "@emotion/react"; import Button from "@mui/material/Button"; -import FormControlLabel from "@mui/material/FormControlLabel"; import InputAdornment from "@mui/material/InputAdornment"; -import Link from "@mui/material/Link"; -import Switch from "@mui/material/Switch"; import TextField from "@mui/material/TextField"; import { useFormik } from "formik"; -import { type FC, useState } from "react"; -import { BlockPicker } from "react-color"; +import type { FC } from "react"; import type { UpdateAppearanceConfig } from "api/typesGenerated"; import { Badges, @@ -15,35 +10,29 @@ import { EnterpriseBadge, EntitledBadge, } from "components/Badges/Badges"; -import { Stack } from "components/Stack/Stack"; -import colors from "theme/tailwindColors"; import { getFormHelpers } from "utils/formUtils"; import { Fieldset } from "../Fieldset"; import { Header } from "../Header"; +import { NotificationBannerSettings } from "./NotificationBannerSettings"; export type AppearanceSettingsPageViewProps = { appearance: UpdateAppearanceConfig; isEntitled: boolean; onSaveAppearance: ( newConfig: Partial<UpdateAppearanceConfig>, - preview: boolean, - ) => void; + ) => Promise<void>; }; -const fallbackBgColor = colors.neutral[500]; - export const AppearanceSettingsPageView: FC< AppearanceSettingsPageViewProps > = ({ appearance, isEntitled, onSaveAppearance }) => { - const theme = useTheme(); - const applicationNameForm = useFormik<{ application_name: string; }>({ initialValues: { application_name: appearance.application_name, }, - onSubmit: (values) => onSaveAppearance(values, false), + onSubmit: (values) => onSaveAppearance(values), }); const applicationNameFieldHelpers = getFormHelpers(applicationNameForm); @@ -53,33 +42,10 @@ export const AppearanceSettingsPageView: FC< initialValues: { logo_url: appearance.logo_url, }, - onSubmit: (values) => onSaveAppearance(values, false), + onSubmit: (values) => onSaveAppearance(values), }); const logoFieldHelpers = getFormHelpers(logoForm); - const serviceBannerForm = useFormik<UpdateAppearanceConfig["service_banner"]>( - { - initialValues: { - message: appearance.service_banner.message, - enabled: appearance.service_banner.enabled, - background_color: - appearance.service_banner.background_color ?? fallbackBgColor, - }, - onSubmit: (values) => - onSaveAppearance( - { - service_banner: values, - }, - false, - ), - }, - ); - const serviceBannerFieldHelpers = getFormHelpers(serviceBannerForm); - - const [backgroundColor, setBackgroundColor] = useState( - serviceBannerForm.values.background_color, - ); - return ( <> <Header @@ -159,123 +125,13 @@ export const AppearanceSettingsPageView: FC< /> </Fieldset> - <Fieldset - title="Service Banner" - subtitle="Configure a banner that displays a message to all users." - onSubmit={serviceBannerForm.handleSubmit} - button={ - !isEntitled && ( - <Button - onClick={() => { - onSaveAppearance( - { - service_banner: { - message: - "👋 **This** is a service banner. The banner's color and text are editable.", - background_color: "#004852", - enabled: true, - }, - }, - true, - ); - }} - > - Show Preview - </Button> - ) - } - validation={ - !isEntitled && ( - <p> - Your license does not include Service Banners.{" "} - <Link href="mailto:sales@coder.com">Contact sales</Link> to learn - more. - </p> - ) + <NotificationBannerSettings + isEntitled={isEntitled} + notificationBanners={appearance.notification_banners || []} + onSubmit={(notificationBanners) => + onSaveAppearance({ notification_banners: notificationBanners }) } - > - {isEntitled && ( - <Stack> - <FormControlLabel - control={ - <Switch - checked={serviceBannerForm.values.enabled} - onChange={async () => { - const newState = !serviceBannerForm.values.enabled; - const newBanner = { - ...serviceBannerForm.values, - enabled: newState, - }; - onSaveAppearance( - { - service_banner: newBanner, - }, - false, - ); - await serviceBannerForm.setFieldValue("enabled", newState); - }} - data-testid="switch-service-banner" - /> - } - label="Enabled" - /> - <Stack spacing={0}> - <TextField - {...serviceBannerFieldHelpers("message", { - helperText: - "Markdown bold, italics, and links are supported.", - })} - fullWidth - label="Message" - multiline - inputProps={{ - "aria-label": "Message", - }} - /> - </Stack> - - <Stack spacing={0}> - <h3>{"Background Color"}</h3> - <BlockPicker - color={backgroundColor} - onChange={async (color) => { - setBackgroundColor(color.hex); - await serviceBannerForm.setFieldValue( - "background_color", - color.hex, - ); - onSaveAppearance( - { - service_banner: { - ...serviceBannerForm.values, - background_color: color.hex, - }, - }, - true, - ); - }} - triangle="hide" - colors={["#004852", "#D65D0F", "#4CD473", "#D94A5D", "#5A00CF"]} - styles={{ - default: { - input: { - color: "white", - backgroundColor: theme.palette.background.default, - }, - body: { - backgroundColor: "black", - color: "white", - }, - card: { - backgroundColor: "black", - }, - }, - }} - /> - </Stack> - </Stack> - )} - </Fieldset> + /> </> ); }; diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerDialog.stories.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerDialog.stories.tsx new file mode 100644 index 0000000000000..d9ae43a6d80d0 --- /dev/null +++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerDialog.stories.tsx @@ -0,0 +1,24 @@ +import { action } from "@storybook/addon-actions"; +import type { Meta, StoryObj } from "@storybook/react"; +import { NotificationBannerDialog } from "./NotificationBannerDialog"; + +const meta: Meta<typeof NotificationBannerDialog> = { + title: "pages/DeploySettingsPage/NotificationBannerDialog", + component: NotificationBannerDialog, + args: { + banner: { + enabled: true, + message: "The beep-bop will be boop-beeped on Saturday at 12AM PST.", + background_color: "#ffaff3", + }, + onCancel: action("onCancel"), + onUpdate: () => Promise.resolve(void action("onUpdate")), + }, +}; + +export default meta; +type Story = StoryObj<typeof NotificationBannerDialog>; + +const Example: Story = {}; + +export { Example as NotificationBannerDialog }; diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerDialog.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerDialog.tsx new file mode 100644 index 0000000000000..6b5ffaf6fc27b --- /dev/null +++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerDialog.tsx @@ -0,0 +1,138 @@ +import { type Interpolation, type Theme, useTheme } from "@emotion/react"; +import DialogActions from "@mui/material/DialogActions"; +import TextField from "@mui/material/TextField"; +import { useFormik } from "formik"; +import type { FC } from "react"; +import { BlockPicker } from "react-color"; +import type { BannerConfig } from "api/typesGenerated"; +import { Dialog, DialogActionButtons } from "components/Dialogs/Dialog"; +import { Stack } from "components/Stack/Stack"; +import { NotificationBannerView } from "modules/dashboard/NotificationBanners/NotificationBannerView"; +import { getFormHelpers } from "utils/formUtils"; + +interface NotificationBannerDialogProps { + banner: BannerConfig; + onCancel: () => void; + onUpdate: (banner: Partial<BannerConfig>) => Promise<void>; +} + +export const NotificationBannerDialog: FC<NotificationBannerDialogProps> = ({ + banner, + onCancel, + onUpdate, +}) => { + const theme = useTheme(); + + const bannerForm = useFormik<{ + message: string; + background_color: string; + }>({ + initialValues: { + message: banner.message ?? "", + background_color: banner.background_color ?? "#004852", + }, + onSubmit: (banner) => onUpdate(banner), + }); + const bannerFieldHelpers = getFormHelpers(bannerForm); + + return ( + <Dialog css={styles.dialogWrapper} open onClose={onCancel}> + {/* Banner preview */} + <div css={{ position: "fixed", top: 0, left: 0, right: 0 }}> + <NotificationBannerView + message={bannerForm.values.message} + backgroundColor={bannerForm.values.background_color} + /> + </div> + + <div css={styles.dialogContent}> + <h3 css={styles.dialogTitle}>Notification banner</h3> + <Stack> + <div> + <h4 css={styles.settingName}>Message</h4> + <TextField + {...bannerFieldHelpers("message", { + helperText: "Markdown bold, italics, and links are supported.", + })} + fullWidth + inputProps={{ + "aria-label": "Message", + placeholder: "Enter a message for the banner", + }} + /> + </div> + <div> + <h4 css={styles.settingName}>Background color</h4> + <BlockPicker + color={bannerForm.values.background_color} + onChange={async (color) => { + await bannerForm.setFieldValue("background_color", color.hex); + }} + triangle="hide" + colors={["#004852", "#D65D0F", "#4CD473", "#D94A5D", "#5A00CF"]} + styles={{ + default: { + input: { + color: "white", + backgroundColor: theme.palette.background.default, + }, + body: { + backgroundColor: "black", + color: "white", + }, + card: { + backgroundColor: "black", + }, + }, + }} + /> + </div> + </Stack> + </div> + + <DialogActions> + <DialogActionButtons + cancelText="Cancel" + confirmLoading={bannerForm.isSubmitting} + confirmText="Update" + disabled={bannerForm.isSubmitting} + onCancel={onCancel} + onConfirm={bannerForm.handleSubmit} + /> + </DialogActions> + </Dialog> + ); +}; + +const styles = { + dialogWrapper: (theme) => ({ + "& .MuiPaper-root": { + background: theme.palette.background.paper, + border: `1px solid ${theme.palette.divider}`, + width: "100%", + maxWidth: 500, + }, + "& .MuiDialogActions-spacing": { + padding: "0 40px 40px", + }, + }), + dialogContent: (theme) => ({ + color: theme.palette.text.secondary, + padding: "40px 40px 20px", + }), + dialogTitle: (theme) => ({ + margin: 0, + marginBottom: 16, + color: theme.palette.text.primary, + fontWeight: 400, + fontSize: 20, + }), + settingName: (theme) => ({ + marginTop: 0, + marginBottom: 8, + color: theme.palette.text.primary, + fontSize: 16, + lineHeight: "150%", + fontWeight: 600, + }), +} satisfies Record<string, Interpolation<Theme>>; diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerItem.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerItem.tsx new file mode 100644 index 0000000000000..76636a30c4492 --- /dev/null +++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerItem.tsx @@ -0,0 +1,77 @@ +import type { Interpolation, Theme } from "@emotion/react"; +import Checkbox from "@mui/material/Checkbox"; +import TableCell from "@mui/material/TableCell"; +import TableRow from "@mui/material/TableRow"; +import type { FC } from "react"; +import type { BannerConfig } from "api/typesGenerated"; +import { + MoreMenu, + MoreMenuContent, + MoreMenuItem, + MoreMenuTrigger, + ThreeDotsButton, +} from "components/MoreMenu/MoreMenu"; + +interface NotificationBannerItemProps { + enabled: boolean; + backgroundColor?: string; + message?: string; + onUpdate: (banner: Partial<BannerConfig>) => Promise<void>; + onEdit: () => void; + onDelete: () => void; +} + +export const NotificationBannerItem: FC<NotificationBannerItemProps> = ({ + enabled, + backgroundColor = "#004852", + message, + onUpdate, + onEdit, + onDelete, +}) => { + return ( + <TableRow> + <TableCell> + <Checkbox + size="small" + checked={enabled} + onClick={() => void onUpdate({ enabled: !enabled })} + /> + </TableCell> + + <TableCell css={!enabled && styles.disabled}> + {message || <em>No message</em>} + </TableCell> + + <TableCell> + <div css={styles.colorSample} style={{ backgroundColor }}></div> + </TableCell> + + <TableCell> + <MoreMenu> + <MoreMenuTrigger> + <ThreeDotsButton /> + </MoreMenuTrigger> + <MoreMenuContent> + <MoreMenuItem onClick={() => onEdit()}>Edit…</MoreMenuItem> + <MoreMenuItem onClick={() => onDelete()} danger> + Delete… + </MoreMenuItem> + </MoreMenuContent> + </MoreMenu> + </TableCell> + </TableRow> + ); +}; + +const styles = { + disabled: (theme) => ({ + color: theme.roles.inactive.fill.outline, + }), + + colorSample: { + width: 24, + height: 24, + borderRadius: 4, + }, +} satisfies Record<string, Interpolation<Theme>>; diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerSettings.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerSettings.tsx new file mode 100644 index 0000000000000..d5611af119614 --- /dev/null +++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerSettings.tsx @@ -0,0 +1,202 @@ +import { type CSSObject, useTheme } from "@emotion/react"; +import AddIcon from "@mui/icons-material/AddOutlined"; +import Button from "@mui/material/Button"; +import Link from "@mui/material/Link"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import { type FC, useState } from "react"; +import type { BannerConfig } from "api/typesGenerated"; +import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; +import { EmptyState } from "components/EmptyState/EmptyState"; +import { Stack } from "components/Stack/Stack"; +import { NotificationBannerDialog } from "./NotificationBannerDialog"; +import { NotificationBannerItem } from "./NotificationBannerItem"; + +interface NotificationBannerSettingsProps { + isEntitled: boolean; + notificationBanners: readonly BannerConfig[]; + onSubmit: (banners: readonly BannerConfig[]) => Promise<void>; +} + +export const NotificationBannerSettings: FC< + NotificationBannerSettingsProps +> = ({ isEntitled, notificationBanners, onSubmit }) => { + const theme = useTheme(); + const [banners, setBanners] = useState(notificationBanners); + const [editingBannerId, setEditingBannerId] = useState<number | null>(null); + const [deletingBannerId, setDeletingBannerId] = useState<number | null>(null); + + const addBanner = () => { + setBanners([ + ...banners, + { enabled: true, message: "", background_color: "#004852" }, + ]); + setEditingBannerId(banners.length); + }; + + const updateBanner = (i: number, banner: Partial<BannerConfig>) => { + const newBanners = [...banners]; + newBanners[i] = { ...banners[i], ...banner }; + setBanners(newBanners); + return newBanners; + }; + + const removeBanner = (i: number) => { + const newBanners = [...banners]; + newBanners.splice(i, 1); + setBanners(newBanners); + return newBanners; + }; + + const editingBanner = editingBannerId !== null && banners[editingBannerId]; + const deletingBanner = deletingBannerId !== null && banners[deletingBannerId]; + + // If we're not editing a new banner, remove all empty banners. This makes canceling the + // "new" dialog more intuitive, by not persisting an empty banner. + if (editingBannerId === null && banners.some((banner) => !banner.message)) { + setBanners(banners.filter((banner) => banner.message)); + } + + return ( + <> + <div + css={{ + borderRadius: 8, + border: `1px solid ${theme.palette.divider}`, + marginTop: 32, + overflow: "hidden", + }} + > + <div css={{ padding: "24px 24px 0" }}> + <Stack + direction="row" + justifyContent="space-between" + alignItems="center" + > + <h3 + css={{ + fontSize: 20, + margin: 0, + fontWeight: 600, + }} + > + Notification Banners + </h3> + <Button + disabled={!isEntitled} + onClick={() => addBanner()} + startIcon={<AddIcon />} + > + New + </Button> + </Stack> + <div + css={{ + color: theme.palette.text.secondary, + fontSize: 14, + marginTop: 8, + }} + > + Display message banners to all users. + </div> + + <div + css={[ + theme.typography.body2 as CSSObject, + { paddingTop: 16, margin: "0 -32px" }, + ]} + > + <TableContainer css={{ borderRadius: 0, borderBottom: "none" }}> + <Table> + <TableHead> + <TableRow> + <TableCell width="1%">Enabled</TableCell> + <TableCell>Message</TableCell> + <TableCell width="2%">Color</TableCell> + <TableCell width="1%" /> + </TableRow> + </TableHead> + <TableBody> + {!isEntitled || banners.length < 1 ? ( + <TableCell colSpan={999}> + <EmptyState + css={{ minHeight: 160 }} + message="No notification banners" + /> + </TableCell> + ) : ( + banners.map((banner, i) => ( + <NotificationBannerItem + key={banner.message} + enabled={banner.enabled && Boolean(banner.message)} + backgroundColor={banner.background_color} + message={banner.message} + onEdit={() => setEditingBannerId(i)} + onUpdate={async (banner) => { + const newBanners = updateBanner(i, banner); + await onSubmit(newBanners); + }} + onDelete={() => setDeletingBannerId(i)} + /> + )) + )} + </TableBody> + </Table> + </TableContainer> + </div> + </div> + + {!isEntitled && ( + <footer + css={[ + theme.typography.body2 as CSSObject, + { + background: theme.palette.background.paper, + padding: "16px 24px", + }, + ]} + > + <div css={{ color: theme.palette.text.secondary }}> + <p> + Your license does not include Service Banners.{" "} + <Link href="mailto:sales@coder.com">Contact sales</Link> to + learn more. + </p> + </div> + </footer> + )} + </div> + + {editingBanner && ( + <NotificationBannerDialog + banner={editingBanner} + onCancel={() => setEditingBannerId(null)} + onUpdate={async (banner) => { + const newBanners = updateBanner(editingBannerId, banner); + setEditingBannerId(null); + await onSubmit(newBanners); + }} + /> + )} + + {deletingBanner && ( + <ConfirmDialog + type="delete" + open + title="Delete this banner?" + description={deletingBanner.message} + onClose={() => setDeletingBannerId(null)} + onConfirm={async () => { + const newBanners = removeBanner(deletingBannerId); + setDeletingBannerId(null); + await onSubmit(newBanners); + }} + /> + )} + </> + ); +}; diff --git a/site/src/pages/DeploySettingsPage/Fieldset.tsx b/site/src/pages/DeploySettingsPage/Fieldset.tsx index d91ab95f23bae..5ef43a9f36c10 100644 --- a/site/src/pages/DeploySettingsPage/Fieldset.tsx +++ b/site/src/pages/DeploySettingsPage/Fieldset.tsx @@ -12,16 +12,15 @@ interface FieldsetProps { isSubmitting?: boolean; } -export const Fieldset: FC<FieldsetProps> = (props) => { - const { - title, - subtitle, - children, - validation, - button, - onSubmit, - isSubmitting, - } = props; +export const Fieldset: FC<FieldsetProps> = ({ + title, + subtitle, + children, + validation, + button, + onSubmit, + isSubmitting, +}) => { const theme = useTheme(); return ( @@ -30,6 +29,7 @@ export const Fieldset: FC<FieldsetProps> = (props) => { borderRadius: 8, border: `1px solid ${theme.palette.divider}`, marginTop: 32, + overflow: "hidden", }} onSubmit={onSubmit} > diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index 7f4db0efb8888..c321366862264 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -8,12 +8,6 @@ import type { WorkspacePermissions } from "./permissions"; import { Workspace } from "./Workspace"; import { WorkspaceBuildLogsSection } from "./WorkspaceBuildLogsSection"; -const MockedAppearance = { - config: Mocks.MockAppearanceConfig, - isPreview: false, - setPreview: () => {}, -}; - const permissions: WorkspacePermissions = { readWorkspace: true, updateWorkspace: true, @@ -43,7 +37,7 @@ const meta: Meta<typeof Workspace> = { value={{ entitlements: Mocks.MockEntitlementsWithScheduling, experiments: Mocks.MockExperiments, - appearance: MockedAppearance, + appearance: Mocks.MockAppearanceConfig, }} > <ProxyContext.Provider diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index e84dbe617dc84..0331f5290bb73 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -13,7 +13,7 @@ import { Margins } from "components/Margins/Margins"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { useEffectEvent } from "hooks/hookPolyfills"; import { Navbar } from "modules/dashboard/Navbar/Navbar"; -import { ServiceBanner } from "modules/dashboard/ServiceBanner/ServiceBanner"; +import { NotificationBanners } from "modules/dashboard/NotificationBanners/NotificationBanners"; import { workspaceChecks, type WorkspacePermissions } from "./permissions"; import { WorkspaceReadyPage } from "./WorkspaceReadyPage"; @@ -106,7 +106,7 @@ export const WorkspacePage: FC = () => { return ( <> - <ServiceBanner /> + <NotificationBanners /> <div css={{ height: "100%", display: "flex", flexDirection: "column" }}> <Navbar /> {pageError ? ( diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx index 1c7b61558a8cf..11fc39b142448 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx @@ -91,12 +91,6 @@ const allWorkspaces = [ ...Object.values(additionalWorkspaces), ]; -const MockedAppearance = { - config: MockAppearanceConfig, - isPreview: false, - setPreview: () => {}, -}; - type FilterProps = ComponentProps<typeof WorkspacesPageView>["filterProps"]; const defaultFilterProps = getDefaultFilterProps<FilterProps>({ @@ -153,7 +147,7 @@ const meta: Meta<typeof WorkspacesPageView> = { value={{ entitlements: MockEntitlementsWithScheduling, experiments: MockExperiments, - appearance: MockedAppearance, + appearance: MockAppearanceConfig, }} > <Story /> diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 17bb48c1d00bd..6cf97131aba67 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -2355,6 +2355,7 @@ export const MockAppearanceConfig: TypesGen.AppearanceConfig = { service_banner: { enabled: false, }, + notification_banners: [], }; export const MockWorkspaceBuildParameter1: TypesGen.WorkspaceBuildParameter = { diff --git a/site/src/testHelpers/storybook.tsx b/site/src/testHelpers/storybook.tsx index 666d8b91c5c98..4d601e0dd67ef 100644 --- a/site/src/testHelpers/storybook.tsx +++ b/site/src/testHelpers/storybook.tsx @@ -28,11 +28,7 @@ export const withDashboardProvider = ( value={{ entitlements, experiments, - appearance: { - config: MockAppearanceConfig, - isPreview: false, - setPreview: () => {}, - }, + appearance: MockAppearanceConfig, }} > <Story /> diff --git a/tailnet/proto/version.go b/tailnet/proto/version.go index a6040a9feae47..16f324f74fa33 100644 --- a/tailnet/proto/version.go +++ b/tailnet/proto/version.go @@ -6,7 +6,7 @@ import ( const ( CurrentMajor = 2 - CurrentMinor = 0 + CurrentMinor = 1 ) var CurrentVersion = apiversion.New(CurrentMajor, CurrentMinor).WithBackwardCompat(1) diff --git a/tailnet/test/integration/integration_test.go b/tailnet/test/integration/integration_test.go index 76b57fecae651..db7ccf12b6a51 100644 --- a/tailnet/test/integration/integration_test.go +++ b/tailnet/test/integration/integration_test.go @@ -136,7 +136,6 @@ func handleTestSubprocess(t *testing.T) { testName += *clientName } - //nolint:parralleltest t.Run(testName, func(t *testing.T) { log := slogtest.Make(t, nil).Leveled(slog.LevelDebug) switch *role {