mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
chore: remove notifications experiment (#14869)
Notifications have proved stable in the [mainline release of v2.15](https://github.com/coder/coder/releases/tag/v2.15.0), and in preparation for v2.16 we're moving this to stable.
This commit is contained in:
@@ -20,7 +20,6 @@ func createOpts(t *testing.T) *coderdtest.Options {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
dt := coderdtest.DeploymentValues(t)
|
dt := coderdtest.DeploymentValues(t)
|
||||||
dt.Experiments = []string{string(codersdk.ExperimentNotifications)}
|
|
||||||
return &coderdtest.Options{
|
return &coderdtest.Options{
|
||||||
DeploymentValues: dt,
|
DeploymentValues: dt,
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-52
@@ -56,15 +56,16 @@ import (
|
|||||||
|
|
||||||
"cdr.dev/slog"
|
"cdr.dev/slog"
|
||||||
"cdr.dev/slog/sloggers/sloghuman"
|
"cdr.dev/slog/sloggers/sloghuman"
|
||||||
"github.com/coder/coder/v2/coderd/entitlements"
|
|
||||||
"github.com/coder/coder/v2/coderd/notifications/reports"
|
|
||||||
"github.com/coder/coder/v2/coderd/runtimeconfig"
|
|
||||||
"github.com/coder/pretty"
|
"github.com/coder/pretty"
|
||||||
"github.com/coder/quartz"
|
"github.com/coder/quartz"
|
||||||
"github.com/coder/retry"
|
"github.com/coder/retry"
|
||||||
"github.com/coder/serpent"
|
"github.com/coder/serpent"
|
||||||
"github.com/coder/wgtunnel/tunnelsdk"
|
"github.com/coder/wgtunnel/tunnelsdk"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/coderd/entitlements"
|
||||||
|
"github.com/coder/coder/v2/coderd/notifications/reports"
|
||||||
|
"github.com/coder/coder/v2/coderd/runtimeconfig"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/buildinfo"
|
"github.com/coder/coder/v2/buildinfo"
|
||||||
"github.com/coder/coder/v2/cli/clilog"
|
"github.com/coder/coder/v2/cli/clilog"
|
||||||
"github.com/coder/coder/v2/cli/cliui"
|
"github.com/coder/coder/v2/cli/cliui"
|
||||||
@@ -684,10 +685,6 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
|||||||
options.OIDCConfig = oc
|
options.OIDCConfig = oc
|
||||||
}
|
}
|
||||||
|
|
||||||
experiments := coderd.ReadExperiments(
|
|
||||||
options.Logger, options.DeploymentValues.Experiments.Value(),
|
|
||||||
)
|
|
||||||
|
|
||||||
// We'll read from this channel in the select below that tracks shutdown. If it remains
|
// We'll read from this channel in the select below that tracks shutdown. If it remains
|
||||||
// nil, that case of the select will just never fire, but it's important not to have a
|
// nil, that case of the select will just never fire, but it's important not to have a
|
||||||
// "bare" read on this channel.
|
// "bare" read on this channel.
|
||||||
@@ -951,6 +948,33 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
|||||||
return xerrors.Errorf("write config url: %w", err)
|
return xerrors.Errorf("write config url: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Manage notifications.
|
||||||
|
cfg := options.DeploymentValues.Notifications
|
||||||
|
metrics := notifications.NewMetrics(options.PrometheusRegistry)
|
||||||
|
helpers := templateHelpers(options)
|
||||||
|
|
||||||
|
// The enqueuer is responsible for enqueueing notifications to the given store.
|
||||||
|
enqueuer, err := notifications.NewStoreEnqueuer(cfg, options.Database, helpers, logger.Named("notifications.enqueuer"), quartz.NewReal())
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("failed to instantiate notification store enqueuer: %w", err)
|
||||||
|
}
|
||||||
|
options.NotificationsEnqueuer = enqueuer
|
||||||
|
|
||||||
|
// The notification manager is responsible for:
|
||||||
|
// - creating notifiers and managing their lifecycles (notifiers are responsible for dequeueing/sending notifications)
|
||||||
|
// - keeping the store updated with status updates
|
||||||
|
notificationsManager, err := notifications.NewManager(cfg, options.Database, helpers, metrics, logger.Named("notifications.manager"))
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("failed to instantiate notification manager: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint:gocritic // TODO: create own role.
|
||||||
|
notificationsManager.Run(dbauthz.AsSystemRestricted(ctx))
|
||||||
|
|
||||||
|
// Run report generator to distribute periodic reports.
|
||||||
|
notificationReportGenerator := reports.NewReportGenerator(ctx, logger.Named("notifications.report_generator"), options.Database, options.NotificationsEnqueuer, quartz.NewReal())
|
||||||
|
defer notificationReportGenerator.Close()
|
||||||
|
|
||||||
// Since errCh only has one buffered slot, all routines
|
// Since errCh only has one buffered slot, all routines
|
||||||
// sending on it must be wrapped in a select/default to
|
// sending on it must be wrapped in a select/default to
|
||||||
// avoid leaving dangling goroutines waiting for the
|
// avoid leaving dangling goroutines waiting for the
|
||||||
@@ -1007,38 +1031,6 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
|||||||
options.WorkspaceUsageTracker = tracker
|
options.WorkspaceUsageTracker = tracker
|
||||||
defer tracker.Close()
|
defer tracker.Close()
|
||||||
|
|
||||||
// Manage notifications.
|
|
||||||
var (
|
|
||||||
notificationsManager *notifications.Manager
|
|
||||||
)
|
|
||||||
if experiments.Enabled(codersdk.ExperimentNotifications) {
|
|
||||||
cfg := options.DeploymentValues.Notifications
|
|
||||||
metrics := notifications.NewMetrics(options.PrometheusRegistry)
|
|
||||||
helpers := templateHelpers(options)
|
|
||||||
|
|
||||||
// The enqueuer is responsible for enqueueing notifications to the given store.
|
|
||||||
enqueuer, err := notifications.NewStoreEnqueuer(cfg, options.Database, helpers, logger.Named("notifications.enqueuer"), quartz.NewReal())
|
|
||||||
if err != nil {
|
|
||||||
return xerrors.Errorf("failed to instantiate notification store enqueuer: %w", err)
|
|
||||||
}
|
|
||||||
options.NotificationsEnqueuer = enqueuer
|
|
||||||
|
|
||||||
// The notification manager is responsible for:
|
|
||||||
// - creating notifiers and managing their lifecycles (notifiers are responsible for dequeueing/sending notifications)
|
|
||||||
// - keeping the store updated with status updates
|
|
||||||
notificationsManager, err = notifications.NewManager(cfg, options.Database, helpers, metrics, logger.Named("notifications.manager"))
|
|
||||||
if err != nil {
|
|
||||||
return xerrors.Errorf("failed to instantiate notification manager: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// nolint:gocritic // TODO: create own role.
|
|
||||||
notificationsManager.Run(dbauthz.AsSystemRestricted(ctx))
|
|
||||||
|
|
||||||
// Run report generator to distribute periodic reports.
|
|
||||||
notificationReportGenerator := reports.NewReportGenerator(ctx, logger.Named("notifications.report_generator"), options.Database, options.NotificationsEnqueuer, quartz.NewReal())
|
|
||||||
defer notificationReportGenerator.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap the server in middleware that redirects to the access URL if
|
// Wrap the server in middleware that redirects to the access URL if
|
||||||
// the request is not to a local IP.
|
// the request is not to a local IP.
|
||||||
var handler http.Handler = coderAPI.RootHandler
|
var handler http.Handler = coderAPI.RootHandler
|
||||||
@@ -1158,19 +1150,17 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
|||||||
// Cancel any remaining in-flight requests.
|
// Cancel any remaining in-flight requests.
|
||||||
shutdownConns()
|
shutdownConns()
|
||||||
|
|
||||||
if notificationsManager != nil {
|
// Stop the notification manager, which will cause any buffered updates to the store to be flushed.
|
||||||
// Stop the notification manager, which will cause any buffered updates to the store to be flushed.
|
// If the Stop() call times out, messages that were sent but not reflected as such in the store will have
|
||||||
// If the Stop() call times out, messages that were sent but not reflected as such in the store will have
|
// their leases expire after a period of time and will be re-queued for sending.
|
||||||
// their leases expire after a period of time and will be re-queued for sending.
|
// See CODER_NOTIFICATIONS_LEASE_PERIOD.
|
||||||
// See CODER_NOTIFICATIONS_LEASE_PERIOD.
|
cliui.Info(inv.Stdout, "Shutting down notifications manager..."+"\n")
|
||||||
cliui.Info(inv.Stdout, "Shutting down notifications manager..."+"\n")
|
err = shutdownWithTimeout(notificationsManager.Stop, 5*time.Second)
|
||||||
err = shutdownWithTimeout(notificationsManager.Stop, 5*time.Second)
|
if err != nil {
|
||||||
if err != nil {
|
cliui.Warnf(inv.Stderr, "Notifications manager shutdown took longer than 5s, "+
|
||||||
cliui.Warnf(inv.Stderr, "Notifications manager shutdown took longer than 5s, "+
|
"this may result in duplicate notifications being sent: %s\n", err)
|
||||||
"this may result in duplicate notifications being sent: %s\n", err)
|
} else {
|
||||||
} else {
|
cliui.Info(inv.Stdout, "Gracefully shut down notifications manager\n")
|
||||||
cliui.Info(inv.Stdout, "Gracefully shut down notifications manager\n")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shut down provisioners before waiting for WebSockets
|
// Shut down provisioners before waiting for WebSockets
|
||||||
|
|||||||
+4
-6
@@ -37,11 +37,12 @@ import (
|
|||||||
"tailscale.com/util/singleflight"
|
"tailscale.com/util/singleflight"
|
||||||
|
|
||||||
"cdr.dev/slog"
|
"cdr.dev/slog"
|
||||||
|
"github.com/coder/quartz"
|
||||||
|
"github.com/coder/serpent"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/coderd/entitlements"
|
"github.com/coder/coder/v2/coderd/entitlements"
|
||||||
"github.com/coder/coder/v2/coderd/idpsync"
|
"github.com/coder/coder/v2/coderd/idpsync"
|
||||||
"github.com/coder/coder/v2/coderd/runtimeconfig"
|
"github.com/coder/coder/v2/coderd/runtimeconfig"
|
||||||
"github.com/coder/quartz"
|
|
||||||
"github.com/coder/serpent"
|
|
||||||
|
|
||||||
agentproto "github.com/coder/coder/v2/agent/proto"
|
agentproto "github.com/coder/coder/v2/agent/proto"
|
||||||
"github.com/coder/coder/v2/buildinfo"
|
"github.com/coder/coder/v2/buildinfo"
|
||||||
@@ -1257,10 +1258,7 @@ func New(options *Options) *API {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
r.Route("/notifications", func(r chi.Router) {
|
r.Route("/notifications", func(r chi.Router) {
|
||||||
r.Use(
|
r.Use(apiKeyMiddleware)
|
||||||
apiKeyMiddleware,
|
|
||||||
httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentNotifications),
|
|
||||||
)
|
|
||||||
r.Get("/settings", api.notificationsSettings)
|
r.Get("/settings", api.notificationsSettings)
|
||||||
r.Put("/settings", api.putNotificationsSettings)
|
r.Put("/settings", api.putNotificationsSettings)
|
||||||
r.Route("/templates", func(r chi.Router) {
|
r.Route("/templates", func(r chi.Router) {
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ type Manager struct {
|
|||||||
|
|
||||||
runOnce sync.Once
|
runOnce sync.Once
|
||||||
stopOnce sync.Once
|
stopOnce sync.Once
|
||||||
|
doneOnce sync.Once
|
||||||
stop chan any
|
stop chan any
|
||||||
done chan any
|
done chan any
|
||||||
|
|
||||||
@@ -153,7 +154,9 @@ func (m *Manager) Run(ctx context.Context) {
|
|||||||
// events, creating a notifier, and publishing bulk dispatch result updates to the store.
|
// events, creating a notifier, and publishing bulk dispatch result updates to the store.
|
||||||
func (m *Manager) loop(ctx context.Context) error {
|
func (m *Manager) loop(ctx context.Context) error {
|
||||||
defer func() {
|
defer func() {
|
||||||
close(m.done)
|
m.doneOnce.Do(func() {
|
||||||
|
close(m.done)
|
||||||
|
})
|
||||||
m.log.Info(context.Background(), "notification manager stopped")
|
m.log.Info(context.Background(), "notification manager stopped")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -364,7 +367,9 @@ func (m *Manager) Stop(ctx context.Context) error {
|
|||||||
// If the notifier hasn't been started, we don't need to wait for anything.
|
// If the notifier hasn't been started, we don't need to wait for anything.
|
||||||
// This is only really during testing when we want to enqueue messages only but not deliver them.
|
// This is only really during testing when we want to enqueue messages only but not deliver them.
|
||||||
if m.notifier == nil {
|
if m.notifier == nil {
|
||||||
close(m.done)
|
m.doneOnce.Do(func() {
|
||||||
|
close(m.done)
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
m.notifier.stop()
|
m.notifier.stop()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1187,7 +1187,6 @@ func createOpts(t *testing.T) *coderdtest.Options {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
dt := coderdtest.DeploymentValues(t)
|
dt := coderdtest.DeploymentValues(t)
|
||||||
dt.Experiments = []string{string(codersdk.ExperimentNotifications)}
|
|
||||||
return &coderdtest.Options{
|
return &coderdtest.Options{
|
||||||
DeploymentValues: dt,
|
DeploymentValues: dt,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Sto
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = reportFailedWorkspaceBuilds(ctx, logger, db, enqueuer, clk)
|
err = reportFailedWorkspaceBuilds(ctx, logger, tx, enqueuer, clk)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("unable to generate reports with failed workspace builds: %w", err)
|
return xerrors.Errorf("unable to generate reports with failed workspace builds: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ func createOpts(t *testing.T) *coderdtest.Options {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
dt := coderdtest.DeploymentValues(t)
|
dt := coderdtest.DeploymentValues(t)
|
||||||
dt.Experiments = []string{string(codersdk.ExperimentNotifications)}
|
|
||||||
return &coderdtest.Options{
|
return &coderdtest.Options{
|
||||||
DeploymentValues: dt,
|
DeploymentValues: dt,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2901,7 +2901,7 @@ const (
|
|||||||
// users to opt-in to via --experimental='*'.
|
// users to opt-in to via --experimental='*'.
|
||||||
// Experiments that are not ready for consumption by all users should
|
// Experiments that are not ready for consumption by all users should
|
||||||
// not be included here and will be essentially hidden.
|
// not be included here and will be essentially hidden.
|
||||||
var ExperimentsAll = Experiments{ExperimentNotifications}
|
var ExperimentsAll = Experiments{}
|
||||||
|
|
||||||
// Experiments is a list of experiments.
|
// Experiments is a list of experiments.
|
||||||
// Multiple experiments may be enabled at the same time.
|
// Multiple experiments may be enabled at the same time.
|
||||||
|
|||||||
@@ -448,7 +448,6 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
|||||||
// with the below route, we need to register this route without any mounts or groups to make both work.
|
// with the below route, we need to register this route without any mounts or groups to make both work.
|
||||||
r.With(
|
r.With(
|
||||||
apiKeyMiddleware,
|
apiKeyMiddleware,
|
||||||
httpmw.RequireExperiment(api.AGPL.Experiments, codersdk.ExperimentNotifications),
|
|
||||||
httpmw.ExtractNotificationTemplateParam(options.Database),
|
httpmw.ExtractNotificationTemplateParam(options.Database),
|
||||||
).Put("/notifications/templates/{notification_template}/method", api.updateNotificationTemplateMethod)
|
).Put("/notifications/templates/{notification_template}/method", api.updateNotificationTemplateMethod)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ func createOpts(t *testing.T) *coderdenttest.Options {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
dt := coderdtest.DeploymentValues(t)
|
dt := coderdtest.DeploymentValues(t)
|
||||||
dt.Experiments = []string{string(codersdk.ExperimentNotifications)}
|
|
||||||
return &coderdenttest.Options{
|
return &coderdenttest.Options{
|
||||||
Options: &coderdtest.Options{
|
Options: &coderdtest.Options{
|
||||||
DeploymentValues: dt,
|
DeploymentValues: dt,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { docs } from "utils/docs";
|
|||||||
* All types of feature that we are currently supporting. Defined as record to
|
* All types of feature that we are currently supporting. Defined as record to
|
||||||
* ensure that we can't accidentally make typos when writing the badge text.
|
* ensure that we can't accidentally make typos when writing the badge text.
|
||||||
*/
|
*/
|
||||||
const featureStageBadgeTypes = {
|
export const featureStageBadgeTypes = {
|
||||||
beta: "beta",
|
beta: "beta",
|
||||||
experimental: "experimental",
|
experimental: "experimental",
|
||||||
} as const satisfies Record<string, ReactNode>;
|
} as const satisfies Record<string, ReactNode>;
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export const NotificationsPage: FC = () => {
|
|||||||
title="Notifications"
|
title="Notifications"
|
||||||
description="Control delivery methods for notifications on this deployment."
|
description="Control delivery methods for notifications on this deployment."
|
||||||
layout="fluid"
|
layout="fluid"
|
||||||
|
featureStage={"beta"}
|
||||||
>
|
>
|
||||||
<Tabs active={tab}>
|
<Tabs active={tab}>
|
||||||
<TabsList>
|
<TabsList>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import NotificationsIcon from "@mui/icons-material/NotificationsNoneOutlined";
|
|||||||
import Globe from "@mui/icons-material/PublicOutlined";
|
import Globe from "@mui/icons-material/PublicOutlined";
|
||||||
import ApprovalIcon from "@mui/icons-material/VerifiedUserOutlined";
|
import ApprovalIcon from "@mui/icons-material/VerifiedUserOutlined";
|
||||||
import VpnKeyOutlined from "@mui/icons-material/VpnKeyOutlined";
|
import VpnKeyOutlined from "@mui/icons-material/VpnKeyOutlined";
|
||||||
|
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
|
||||||
import { GitIcon } from "components/Icons/GitIcon";
|
import { GitIcon } from "components/Icons/GitIcon";
|
||||||
import {
|
import {
|
||||||
Sidebar as BaseSidebar,
|
Sidebar as BaseSidebar,
|
||||||
@@ -51,11 +52,9 @@ export const Sidebar: FC = () => {
|
|||||||
<SidebarNavItem href="observability" icon={InsertChartIcon}>
|
<SidebarNavItem href="observability" icon={InsertChartIcon}>
|
||||||
Observability
|
Observability
|
||||||
</SidebarNavItem>
|
</SidebarNavItem>
|
||||||
{experiments.includes("notifications") && (
|
<SidebarNavItem href="notifications" icon={NotificationsIcon}>
|
||||||
<SidebarNavItem href="notifications" icon={NotificationsIcon}>
|
Notifications <FeatureStageBadge contentType="beta" size="sm" />
|
||||||
Notifications
|
</SidebarNavItem>
|
||||||
</SidebarNavItem>
|
|
||||||
)}
|
|
||||||
</BaseSidebar>
|
</BaseSidebar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -148,11 +148,12 @@ const DeploymentSettingsNavigation: FC<DeploymentSettingsNavigationProps> = ({
|
|||||||
Users
|
Users
|
||||||
</SidebarNavSubItem>
|
</SidebarNavSubItem>
|
||||||
)}
|
)}
|
||||||
{experiments.includes("notifications") && (
|
<Stack direction="row" alignItems="center" css={{ gap: 0 }}>
|
||||||
<SidebarNavSubItem href="notifications">
|
<SidebarNavSubItem href="notifications">
|
||||||
Notifications
|
Notifications
|
||||||
</SidebarNavSubItem>
|
</SidebarNavSubItem>
|
||||||
)}
|
<FeatureStageBadge contentType="beta" size="sm" />
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export const NotificationsPage: FC = () => {
|
|||||||
title="Notifications"
|
title="Notifications"
|
||||||
description="Configure your notification preferences. Icons on the right of each notification indicate delivery method, either SMTP or Webhook."
|
description="Configure your notification preferences. Icons on the right of each notification indicate delivery method, either SMTP or Webhook."
|
||||||
layout="fluid"
|
layout="fluid"
|
||||||
|
featureStage="beta"
|
||||||
>
|
>
|
||||||
{ready ? (
|
{ready ? (
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import type { Interpolation, Theme } from "@emotion/react";
|
import type { Interpolation, Theme } from "@emotion/react";
|
||||||
|
import {
|
||||||
|
FeatureStageBadge,
|
||||||
|
type featureStageBadgeTypes,
|
||||||
|
} from "components/FeatureStageBadge/FeatureStageBadge";
|
||||||
|
import { Stack } from "components/Stack/Stack";
|
||||||
import type { FC, ReactNode } from "react";
|
import type { FC, ReactNode } from "react";
|
||||||
|
|
||||||
type SectionLayout = "fixed" | "fluid";
|
type SectionLayout = "fixed" | "fluid";
|
||||||
@@ -13,6 +18,7 @@ export interface SectionProps {
|
|||||||
layout?: SectionLayout;
|
layout?: SectionLayout;
|
||||||
className?: string;
|
className?: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
featureStage?: keyof typeof featureStageBadgeTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Section: FC<SectionProps> = ({
|
export const Section: FC<SectionProps> = ({
|
||||||
@@ -24,6 +30,7 @@ export const Section: FC<SectionProps> = ({
|
|||||||
className = "",
|
className = "",
|
||||||
children,
|
children,
|
||||||
layout = "fixed",
|
layout = "fixed",
|
||||||
|
featureStage,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<section className={className} id={id} data-testid={id}>
|
<section className={className} id={id} data-testid={id}>
|
||||||
@@ -32,16 +39,25 @@ export const Section: FC<SectionProps> = ({
|
|||||||
<div css={styles.header}>
|
<div css={styles.header}>
|
||||||
<div>
|
<div>
|
||||||
{title && (
|
{title && (
|
||||||
<h4
|
<Stack direction={"row"} alignItems="center">
|
||||||
css={{
|
<h4
|
||||||
fontSize: 24,
|
css={{
|
||||||
fontWeight: 500,
|
fontSize: 24,
|
||||||
margin: 0,
|
fontWeight: 500,
|
||||||
marginBottom: 8,
|
margin: 0,
|
||||||
}}
|
marginBottom: 8,
|
||||||
>
|
}}
|
||||||
{title}
|
>
|
||||||
</h4>
|
{title}
|
||||||
|
</h4>
|
||||||
|
{featureStage && (
|
||||||
|
<FeatureStageBadge
|
||||||
|
contentType={featureStage}
|
||||||
|
size="lg"
|
||||||
|
css={{ marginBottom: "5px" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{description && typeof description === "string" && (
|
{description && typeof description === "string" && (
|
||||||
<p css={styles.description}>{description}</p>
|
<p css={styles.description}>{description}</p>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import NotificationsIcon from "@mui/icons-material/NotificationsNoneOutlined";
|
|||||||
import AccountIcon from "@mui/icons-material/Person";
|
import AccountIcon from "@mui/icons-material/Person";
|
||||||
import VpnKeyOutlined from "@mui/icons-material/VpnKeyOutlined";
|
import VpnKeyOutlined from "@mui/icons-material/VpnKeyOutlined";
|
||||||
import type { User } from "api/typesGenerated";
|
import type { User } from "api/typesGenerated";
|
||||||
|
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
|
||||||
import { GitIcon } from "components/Icons/GitIcon";
|
import { GitIcon } from "components/Icons/GitIcon";
|
||||||
import {
|
import {
|
||||||
Sidebar as BaseSidebar,
|
Sidebar as BaseSidebar,
|
||||||
@@ -57,11 +58,9 @@ export const Sidebar: FC<SidebarProps> = ({ user }) => {
|
|||||||
<SidebarNavItem href="tokens" icon={VpnKeyOutlined}>
|
<SidebarNavItem href="tokens" icon={VpnKeyOutlined}>
|
||||||
Tokens
|
Tokens
|
||||||
</SidebarNavItem>
|
</SidebarNavItem>
|
||||||
{experiments.includes("notifications") && (
|
<SidebarNavItem href="notifications" icon={NotificationsIcon}>
|
||||||
<SidebarNavItem href="notifications" icon={NotificationsIcon}>
|
Notifications <FeatureStageBadge contentType="beta" size="sm" />
|
||||||
Notifications
|
</SidebarNavItem>
|
||||||
</SidebarNavItem>
|
|
||||||
)}
|
|
||||||
</BaseSidebar>
|
</BaseSidebar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user