mirror of https://github.com/coder/coder.git
Merge 0a3a01d603
into 93d8812284
This commit is contained in:
commit
a03152793a
|
@ -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
|
||||
ServiceBanner 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)
|
||||
|
@ -124,7 +124,7 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom
|
|||
config.MOTDFile = func() string { return "" }
|
||||
}
|
||||
if config.ServiceBanner == nil {
|
||||
config.ServiceBanner = func() *codersdk.ServiceBannerConfig { return &codersdk.ServiceBannerConfig{} }
|
||||
config.ServiceBanner = func() *codersdk.BannerConfig { return &codersdk.BannerConfig{} }
|
||||
}
|
||||
if config.WorkingDirectory == nil {
|
||||
config.WorkingDirectory = func() string {
|
||||
|
|
|
@ -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": "ServiceBanner is for a single banner, and has been replaced by NotificationBanners. Deprecated.",
|
||||
"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": {
|
||||
|
@ -11058,20 +11083,6 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ServiceBannerConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"background_color": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.SessionCountDeploymentStats": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -11904,8 +11915,19 @@ const docTemplate = `{
|
|||
"logo_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"notification_banners": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.BannerConfig"
|
||||
}
|
||||
},
|
||||
"service_banner": {
|
||||
"$ref": "#/definitions/codersdk.ServiceBannerConfig"
|
||||
"description": "ServiceBanner is for a single banner, and has been replaced by NotificationBanners. Deprecated.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.BannerConfig"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -7341,8 +7341,19 @@
|
|||
"logo_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"notification_banners": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.BannerConfig"
|
||||
}
|
||||
},
|
||||
"service_banner": {
|
||||
"$ref": "#/definitions/codersdk.ServiceBannerConfig"
|
||||
"description": "ServiceBanner is for a single banner, and has been replaced by NotificationBanners. Deprecated.",
|
||||
"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": "ServiceBanner is for a single banner, and has been replaced by NotificationBanners. Deprecated.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.BannerConfig"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
@ -3364,6 +3369,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
|
||||
|
|
|
@ -528,6 +528,9 @@ func (s *MethodTestSuite) TestLicense() {
|
|||
s.Run("UpsertServiceBanner", s.Subtest(func(db database.Store, check *expects) {
|
||||
check.Args("value").Asserts(rbac.ResourceDeploymentValues, rbac.ActionCreate)
|
||||
}))
|
||||
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) {
|
||||
l, err := db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
||||
UUID: uuid.New(),
|
||||
|
@ -561,6 +564,11 @@ func (s *MethodTestSuite) TestLicense() {
|
|||
require.NoError(s.T(), err)
|
||||
check.Args().Asserts().Returns("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")
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *MethodTestSuite) TestOrganization() {
|
||||
|
|
|
@ -186,6 +186,7 @@ type data struct {
|
|||
derpMeshKey string
|
||||
lastUpdateCheck []byte
|
||||
serviceBanner []byte
|
||||
notificationBanners []byte
|
||||
healthSettings []byte
|
||||
applicationName string
|
||||
logoURL string
|
||||
|
@ -2488,6 +2489,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()
|
||||
|
@ -8251,6 +8263,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()
|
||||
|
|
|
@ -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)
|
||||
|
@ -2186,6 +2193,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)
|
||||
|
|
|
@ -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()
|
||||
|
@ -4577,6 +4592,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()
|
||||
|
|
|
@ -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)
|
||||
|
@ -421,6 +422,7 @@ 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
|
||||
|
|
|
@ -5615,6 +5615,17 @@ func (q *sqlQuerier) GetLogoURL(ctx context.Context) (string, error) {
|
|||
return value, err
|
||||
}
|
||||
|
||||
const getNotificationBanners = `-- name: GetNotificationBanners :one
|
||||
SELECT value FROM site_configs WHERE key = 'notification_banners'
|
||||
`
|
||||
|
||||
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 getOAuthSigningKey = `-- name: GetOAuthSigningKey :one
|
||||
SELECT value FROM site_configs WHERE key = 'oauth_signing_key'
|
||||
`
|
||||
|
@ -5728,6 +5739,16 @@ func (q *sqlQuerier) UpsertLogoURL(ctx context.Context, value string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
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) UpsertNotificationBanners(ctx context.Context, value string) error {
|
||||
_, err := q.db.ExecContext(ctx, upsertNotificationBanners, value)
|
||||
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'
|
||||
|
|
|
@ -43,6 +43,13 @@ ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'service_ban
|
|||
-- name: GetServiceBanner :one
|
||||
SELECT value FROM site_configs WHERE 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: GetNotificationBanners :one
|
||||
SELECT value FROM site_configs WHERE key = 'notification_banners';
|
||||
|
||||
-- name: UpsertLogoURL :exec
|
||||
INSERT INTO site_configs (key, value) VALUES ('logo_url', $1)
|
||||
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'logo_url';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -2084,19 +2084,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"`
|
||||
// ServiceBanner is for a single banner, and has been replaced by NotificationBanners. Deprecated.
|
||||
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"`
|
||||
// ServiceBanner is for a single banner, and has been replaced by NotificationBanners. Deprecated.
|
||||
ServiceBanner BannerConfig `json:"service_banner"`
|
||||
NotificationBanners []BannerConfig `json:"notification_banners"`
|
||||
}
|
||||
|
||||
type ServiceBannerConfig struct {
|
||||
// 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"`
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 | | Service banner is for a single banner, and has been replaced by NotificationBanners. Deprecated. |
|
||||
| `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
|
||||
|
@ -4265,24 +4291,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
|
||||
|
@ -5175,6 +5183,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,
|
||||
|
@ -5185,11 +5200,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 | | Service banner is for a single banner, and has been replaced by NotificationBanners. Deprecated. |
|
||||
|
||||
## codersdk.UpdateCheckResponse
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
@ -53,9 +54,12 @@ 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
|
||||
serviceBannerJSON string
|
||||
notificationBannersJSON string
|
||||
)
|
||||
eg.Go(func() (err error) {
|
||||
applicationName, err = f.database.GetApplicationName(ctx)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
|
@ -77,27 +81,42 @@ func (f *appearanceFetcher) Fetch(ctx context.Context) (codersdk.AppearanceConfi
|
|||
}
|
||||
return nil
|
||||
})
|
||||
eg.Go(func() (err error) {
|
||||
notificationBannersJSON, err = f.database.GetNotificationBanners(ctx)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return xerrors.Errorf("get notification banners: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
err := eg.Wait()
|
||||
if err != nil {
|
||||
return codersdk.AppearanceConfig{}, err
|
||||
}
|
||||
|
||||
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 err != nil {
|
||||
return codersdk.AppearanceConfig{}, xerrors.Errorf(
|
||||
"unmarshal json: %w, raw: %s", err, serviceBannerJSON,
|
||||
"unmarshal service banner json: %w, raw: %s", err, serviceBannerJSON,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.supportLinks) == 0 {
|
||||
cfg.SupportLinks = agpl.DefaultSupportLinks
|
||||
} else {
|
||||
if notificationBannersJSON != "" {
|
||||
err = json.Unmarshal([]byte(notificationBannersJSON), &cfg.NotificationBanners)
|
||||
if err != nil {
|
||||
return codersdk.AppearanceConfig{}, xerrors.Errorf(
|
||||
"unmarshal notification banners json: %w, raw: %s", err, notificationBannersJSON,
|
||||
)
|
||||
}
|
||||
}
|
||||
if len(f.supportLinks) > 0 {
|
||||
cfg.SupportLinks = f.supportLinks
|
||||
}
|
||||
|
||||
|
@ -142,7 +161,7 @@ func (api *API) putAppearance(rw http.ResponseWriter, r *http.Request) {
|
|||
if appearance.ServiceBanner.Enabled {
|
||||
if err := validateHexColor(appearance.ServiceBanner.BackgroundColor); err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid color format",
|
||||
Message: fmt.Sprintf("Invalid color format: %q", appearance.ServiceBanner.BackgroundColor),
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
|
@ -167,6 +186,34 @@ func (api *API) putAppearance(rw http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
for _, banner := range appearance.NotificationBanners {
|
||||
if err := validateHexColor(banner.BackgroundColor); err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("Invalid color format: %q", banner.BackgroundColor),
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
notificationBannersJSON, err := json.Marshal(appearance.NotificationBanners)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Unable to marshal notification banners",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err = api.Database.UpsertNotificationBanners(ctx, string(notificationBannersJSON))
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Unable to set notification banners",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err = api.Database.UpsertApplicationName(ctx, appearance.ApplicationName)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
|
|
|
@ -141,7 +141,7 @@ func TestServiceBanners(t *testing.T) {
|
|||
},
|
||||
})
|
||||
cfg := codersdk.UpdateAppearanceConfig{
|
||||
ServiceBanner: codersdk.ServiceBannerConfig{
|
||||
ServiceBanner: codersdk.BannerConfig{
|
||||
Enabled: true,
|
||||
Message: "Hey",
|
||||
BackgroundColor: "#00FF00",
|
||||
|
@ -165,17 +165,17 @@ func TestServiceBanners(t *testing.T) {
|
|||
agplAgentClient := agentsdk.New(agplClient.URL)
|
||||
agplAgentClient.SetSessionToken(r.AgentToken)
|
||||
banner = requireGetServiceBanner(ctx, t, agplAgentClient)
|
||||
require.Equal(t, codersdk.ServiceBannerConfig{}, banner)
|
||||
require.Equal(t, codersdk.BannerConfig{}, banner)
|
||||
|
||||
// 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)
|
||||
require.Equal(t, codersdk.BannerConfig{}, banner)
|
||||
})
|
||||
}
|
||||
|
||||
func requireGetServiceBanner(ctx context.Context, t *testing.T, client *agentsdk.Client) codersdk.ServiceBannerConfig {
|
||||
func requireGetServiceBanner(ctx context.Context, t *testing.T, client *agentsdk.Client) codersdk.BannerConfig {
|
||||
cc, err := client.ConnectRPC(ctx)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
|
|
|
@ -1357,6 +1357,7 @@ export const getAppearance = async (): Promise<TypesGen.AppearanceConfig> => {
|
|||
service_banner: {
|
||||
enabled: false,
|
||||
},
|
||||
notification_banners: [],
|
||||
};
|
||||
}
|
||||
throw ex;
|
||||
|
|
|
@ -5,13 +5,13 @@ import { getMetadataAsJSON } from "utils/metadata";
|
|||
import { cachedQuery } from "./util";
|
||||
|
||||
const initialAppearanceData = getMetadataAsJSON<AppearanceConfig>("appearance");
|
||||
const appearanceConfigKey = ["appearance"] as const;
|
||||
export const appearanceConfigKey = ["appearance"] as const;
|
||||
|
||||
export const appearance = (): UseQueryOptions<AppearanceConfig> => {
|
||||
// We either have our initial data or should immediately fetch and never again!
|
||||
return cachedQuery({
|
||||
initialData: initialAppearanceData,
|
||||
queryKey: ["appearance"],
|
||||
queryKey: appearanceConfigKey,
|
||||
queryFn: () => API.getAppearance(),
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -7,6 +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 { NotificationBanners } from "modules/dashboard/NotificationBanners/NotificationBanners";
|
||||
import { ServiceBanner } from "modules/dashboard/ServiceBanner/ServiceBanner";
|
||||
import { dashboardContentBottomPadding } from "theme/constants";
|
||||
import { docs } from "utils/docs";
|
||||
|
@ -21,8 +22,9 @@ export const DashboardLayout: FC = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<ServiceBanner />
|
||||
{canViewDeployment && <LicenseBanner />}
|
||||
<ServiceBanner />
|
||||
<NotificationBanners />
|
||||
|
||||
<div
|
||||
css={{
|
||||
|
|
|
@ -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: "weeeee",
|
||||
backgroundColor: "#FFFFFF",
|
||||
},
|
||||
};
|
||||
|
||||
export const Preview: Story = {
|
||||
args: {
|
||||
message: "weeeee",
|
||||
backgroundColor: "#000000",
|
||||
},
|
||||
};
|
|
@ -0,0 +1,48 @@
|
|||
import { css, type Interpolation, type Theme } from "@emotion/react";
|
||||
import type { FC } from "react";
|
||||
import { InlineMarkdown } from "components/Markdown/Markdown";
|
||||
import { readableForegroundColor } from "utils/colors";
|
||||
|
||||
export interface NotificationBannerViewProps {
|
||||
message?: string;
|
||||
backgroundColor?: string;
|
||||
}
|
||||
|
||||
export const NotificationBannerView: FC<NotificationBannerViewProps> = ({
|
||||
message,
|
||||
backgroundColor,
|
||||
}) => {
|
||||
if (message === undefined || backgroundColor === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div css={[styles.banner, { backgroundColor }]} className="service-banner">
|
||||
<div
|
||||
css={[
|
||||
styles.wrapper,
|
||||
{ color: readableForegroundColor(backgroundColor) },
|
||||
]}
|
||||
>
|
||||
<InlineMarkdown>{message}</InlineMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
banner: css`
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`,
|
||||
wrapper: css`
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
font-weight: 400;
|
||||
|
||||
& a {
|
||||
color: inherit;
|
||||
}
|
||||
`,
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
|
@ -0,0 +1,22 @@
|
|||
import type { FC } from "react";
|
||||
import { useDashboard } from "modules/dashboard/useDashboard";
|
||||
import { NotificationBannerView } from "./NotificationBannerView";
|
||||
|
||||
export const NotificationBanners: FC = () => {
|
||||
const dashboard = useDashboard();
|
||||
const notificationBanners = dashboard.appearance.config.notification_banners;
|
||||
|
||||
return (
|
||||
<>
|
||||
{notificationBanners
|
||||
.filter((banner) => banner.enabled)
|
||||
.map(({ message, background_color }) => (
|
||||
<NotificationBannerView
|
||||
key={message}
|
||||
message={message}
|
||||
backgroundColor={background_color}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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,7 +20,7 @@ const AppearanceSettingsPage: FC = () => {
|
|||
|
||||
const onSaveAppearance = async (
|
||||
newConfig: Partial<UpdateAppearanceConfig>,
|
||||
preview: boolean,
|
||||
preview: boolean = false,
|
||||
) => {
|
||||
const newAppearance = { ...appearance.config, ...newConfig };
|
||||
if (preview) {
|
||||
|
@ -30,6 +30,7 @@ const AppearanceSettingsPage: FC = () => {
|
|||
|
||||
try {
|
||||
await updateAppearanceMutation.mutateAsync(newAppearance);
|
||||
await queryClient.invalidateQueries(appearanceConfigKey);
|
||||
displaySuccess("Successfully updated appearance settings!");
|
||||
} catch (error) {
|
||||
displayError(
|
||||
|
|
|
@ -13,6 +13,13 @@ const meta: Meta<typeof AppearanceSettingsPageView> = {
|
|||
message: "hello world",
|
||||
background_color: "white",
|
||||
},
|
||||
notification_banners: [
|
||||
{
|
||||
enabled: true,
|
||||
message: "hello world",
|
||||
background_color: "white",
|
||||
},
|
||||
],
|
||||
},
|
||||
isEntitled: false,
|
||||
},
|
||||
|
|
|
@ -8,9 +8,10 @@ import TextField from "@mui/material/TextField";
|
|||
import { useFormik } from "formik";
|
||||
import { type FC, useState } from "react";
|
||||
import { BlockPicker } from "react-color";
|
||||
import type { UpdateAppearanceConfig } from "api/typesGenerated";
|
||||
import type { BannerConfig, UpdateAppearanceConfig } from "api/typesGenerated";
|
||||
import {
|
||||
Badges,
|
||||
DeprecatedBadge,
|
||||
DisabledBadge,
|
||||
EnterpriseBadge,
|
||||
EntitledBadge,
|
||||
|
@ -20,13 +21,14 @@ import colors from "theme/tailwindColors";
|
|||
import { getFormHelpers } from "utils/formUtils";
|
||||
import { Fieldset } from "../Fieldset";
|
||||
import { Header } from "../Header";
|
||||
import { NotificationBannerItem } from "./NotificationBannerItem";
|
||||
|
||||
export type AppearanceSettingsPageViewProps = {
|
||||
appearance: UpdateAppearanceConfig;
|
||||
isEntitled: boolean;
|
||||
onSaveAppearance: (
|
||||
newConfig: Partial<UpdateAppearanceConfig>,
|
||||
preview: boolean,
|
||||
preview?: boolean,
|
||||
) => void;
|
||||
};
|
||||
|
||||
|
@ -80,6 +82,36 @@ export const AppearanceSettingsPageView: FC<
|
|||
serviceBannerForm.values.background_color,
|
||||
);
|
||||
|
||||
const [notificationBanners, setNotificationBanners] = useState(
|
||||
appearance.notification_banners,
|
||||
);
|
||||
|
||||
const addNotificationBannerItem = () => {
|
||||
setNotificationBanners((banners) => [
|
||||
...banners,
|
||||
{ enabled: true, message: "foob", background_color: "#004852" },
|
||||
]);
|
||||
};
|
||||
|
||||
const updateNotificationBannerItem = (
|
||||
i: number,
|
||||
banner: Partial<BannerConfig>,
|
||||
) => {
|
||||
setNotificationBanners((banners) => {
|
||||
const newBanners = [...banners];
|
||||
newBanners[i] = { ...banners[i], ...banner };
|
||||
return newBanners;
|
||||
});
|
||||
};
|
||||
|
||||
const removeNotificationBannerItem = (i: number) => {
|
||||
setNotificationBanners((banners) => {
|
||||
const newBanners = [...banners];
|
||||
newBanners.splice(i, 1);
|
||||
return newBanners;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
|
@ -160,7 +192,11 @@ export const AppearanceSettingsPageView: FC<
|
|||
</Fieldset>
|
||||
|
||||
<Fieldset
|
||||
title="Service Banner"
|
||||
title={
|
||||
<Stack direction="row">
|
||||
Service Banner <DeprecatedBadge />
|
||||
</Stack>
|
||||
}
|
||||
subtitle="Configure a banner that displays a message to all users."
|
||||
onSubmit={serviceBannerForm.handleSubmit}
|
||||
button={
|
||||
|
@ -235,7 +271,7 @@ export const AppearanceSettingsPageView: FC<
|
|||
</Stack>
|
||||
|
||||
<Stack spacing={0}>
|
||||
<h3>{"Background Color"}</h3>
|
||||
<h3>Background Color</h3>
|
||||
<BlockPicker
|
||||
color={backgroundColor}
|
||||
onChange={async (color) => {
|
||||
|
@ -276,6 +312,41 @@ export const AppearanceSettingsPageView: FC<
|
|||
</Stack>
|
||||
)}
|
||||
</Fieldset>
|
||||
|
||||
<Fieldset
|
||||
title="Notification Banners"
|
||||
onSubmit={() =>
|
||||
onSaveAppearance({
|
||||
notification_banners: notificationBanners,
|
||||
})
|
||||
}
|
||||
>
|
||||
<>
|
||||
<Button onClick={() => addNotificationBannerItem()}>+</Button>
|
||||
<Stack spacing={4}>
|
||||
{notificationBanners
|
||||
.filter((banner) => banner.enabled)
|
||||
.map((banner, i) => (
|
||||
<NotificationBannerItem
|
||||
key={i}
|
||||
enabled={banner.enabled}
|
||||
backgroundColor={banner.background_color}
|
||||
message={banner.message}
|
||||
onRemove={() => removeNotificationBannerItem(i)}
|
||||
onUpdate={(banner: Partial<BannerConfig>) => {
|
||||
const shouldPersist = "enabled" in banner;
|
||||
updateNotificationBannerItem(i, banner);
|
||||
if (shouldPersist) {
|
||||
onSaveAppearance({
|
||||
notification_banners: notificationBanners,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
</Fieldset>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
import { useTheme } from "@emotion/react";
|
||||
import Delete from "@mui/icons-material/Delete";
|
||||
import Button from "@mui/material/Button";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import type { FC } from "react";
|
||||
import { BlockPicker } from "react-color";
|
||||
import type { BannerConfig } from "api/typesGenerated";
|
||||
import Switch from "@mui/material/Switch";
|
||||
|
||||
interface NotificationBannerItemProps {
|
||||
enabled: boolean;
|
||||
backgroundColor?: string;
|
||||
message?: string;
|
||||
onRemove: () => void;
|
||||
onUpdate: (banner: Partial<BannerConfig>) => void;
|
||||
}
|
||||
|
||||
export const NotificationBannerItem: FC<NotificationBannerItemProps> = ({
|
||||
enabled,
|
||||
backgroundColor,
|
||||
message,
|
||||
onRemove,
|
||||
onUpdate,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => onUpdate({ enabled: !enabled })}
|
||||
data-testid="switch-service-banner"
|
||||
/>
|
||||
<Button onClick={onRemove}>
|
||||
<Delete />
|
||||
</Button>
|
||||
</div>
|
||||
<div css={{ backgroundColor }}>{message}</div>
|
||||
|
||||
<TextField
|
||||
// {...serviceBannerFieldHelpers("message", {
|
||||
// helperText:
|
||||
// ,
|
||||
// })}
|
||||
onChange={(event) => onUpdate({ message: event.target.value })}
|
||||
defaultValue={message}
|
||||
helperText="Markdown bold, italics, and links are supported."
|
||||
fullWidth
|
||||
label="Message"
|
||||
multiline
|
||||
inputProps={{
|
||||
"aria-label": "Message",
|
||||
}}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
css={{
|
||||
backgroundColor,
|
||||
width: 24,
|
||||
height: 24,
|
||||
outline: "none",
|
||||
border: "none",
|
||||
borderRadius: 4,
|
||||
}}
|
||||
></button>
|
||||
<details>
|
||||
<summary>Background Color</summary>
|
||||
<BlockPicker
|
||||
color={backgroundColor}
|
||||
onChange={async (color) => {
|
||||
// TODO: preview the color?
|
||||
onUpdate({ 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",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -2355,6 +2355,7 @@ export const MockAppearanceConfig: TypesGen.AppearanceConfig = {
|
|||
service_banner: {
|
||||
enabled: false,
|
||||
},
|
||||
notification_banners: [],
|
||||
};
|
||||
|
||||
export const MockWorkspaceBuildParameter1: TypesGen.WorkspaceBuildParameter = {
|
||||
|
|
Loading…
Reference in New Issue