diff --git a/lib/auth/grpcserver.go b/lib/auth/grpcserver.go
index f553dfbcda955..741f4626c957c 100644
--- a/lib/auth/grpcserver.go
+++ b/lib/auth/grpcserver.go
@@ -82,6 +82,7 @@ import (
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/api/types/installers"
"github.com/gravitational/teleport/api/types/wrappers"
+ apiutils "github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/lib/auth/accessmonitoringrules/accessmonitoringrulesv1"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/auth/autoupdate/autoupdatev1"
@@ -1977,11 +1978,58 @@ func (g *GRPCServer) DeleteAllKubernetesServers(ctx context.Context, req *authpb
// version for some features of the role returns a shallow copy of the given
// role downgraded for compatibility with the older version.
func maybeDowngradeRole(ctx context.Context, role *types.RoleV6) (*types.RoleV6, error) {
- // Teleport 16 supports all role features that Teleport 15 does,
- // so no downgrade is necessary.
+ clientVersionString, ok := metadata.ClientVersionFromContext(ctx)
+ if !ok {
+ // This client is not reporting its version via gRPC metadata. Teleport
+ // clients have been reporting their version for long enough that older
+ // clients won't even support v6 roles at all, so this is likely a
+ // third-party client, and we shouldn't assume that downgrading the role
+ // will do more good than harm.
+ return role, nil
+ }
+
+ clientVersion, err := semver.NewVersion(clientVersionString)
+ if err != nil {
+ return nil, trace.BadParameter("unrecognized client version: %s is not a valid semver", clientVersionString)
+ }
+
+ role = maybeDowngradeRoleSSHPortForwarding(role, clientVersion)
return role, nil
}
+var minSupportedSSHPortForwardingVersions = map[int64]semver.Version{
+ 17: {Major: 17, Minor: 1, Patch: 0},
+}
+
+func maybeDowngradeRoleSSHPortForwarding(role *types.RoleV6, clientVersion *semver.Version) *types.RoleV6 {
+ sshPortForwarding := role.GetOptions().SSHPortForwarding
+ if sshPortForwarding == nil || (sshPortForwarding.Remote == nil && sshPortForwarding.Local == nil) {
+ return role
+ }
+
+ minSupportedVersion, ok := minSupportedSSHPortForwardingVersions[clientVersion.Major]
+ if ok {
+ if supported, err := utils.MinVerWithoutPreRelease(clientVersion.String(), minSupportedVersion.String()); supported || err != nil {
+ return role
+ }
+ }
+
+ role = apiutils.CloneProtoMsg(role)
+ options := role.GetOptions()
+
+ //nolint:staticcheck // this field is preserved for backwards compatibility
+ options.PortForwarding = types.NewBoolOption(services.RoleSet{role}.CanPortForward())
+ role.SetOptions(options)
+ reason := fmt.Sprintf(`Client version %q does not support granular SSH port forwarding. Role %q will be downgraded `+
+ `to simple port forwarding rules instead. In order to support granular SSH port forwarding, all clients must be `+
+ `updated to version %q or higher.`, clientVersion, role.GetName(), minSupportedVersion)
+ if role.Metadata.Labels == nil {
+ role.Metadata.Labels = make(map[string]string, 1)
+ }
+ role.Metadata.Labels[types.TeleportDowngradedLabel] = reason
+ return role
+}
+
// GetRole retrieves a role by name.
func (g *GRPCServer) GetRole(ctx context.Context, req *authpb.GetRoleRequest) (*types.RoleV6, error) {
auth, err := g.authenticate(ctx)
diff --git a/lib/auth/grpcserver_test.go b/lib/auth/grpcserver_test.go
index ed8e61bd00a85..8a91f952e001e 100644
--- a/lib/auth/grpcserver_test.go
+++ b/lib/auth/grpcserver_test.go
@@ -57,6 +57,7 @@ import (
clusterconfigpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/clusterconfig/v1"
mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1"
"github.com/gravitational/teleport/api/internalutils/stream"
+ "github.com/gravitational/teleport/api/metadata"
"github.com/gravitational/teleport/api/mfa"
"github.com/gravitational/teleport/api/observability/tracing"
"github.com/gravitational/teleport/api/types"
@@ -4604,6 +4605,255 @@ func TestGRPCServer_GetInstallers(t *testing.T) {
}
}
+func TestRoleVersions(t *testing.T) {
+ t.Parallel()
+ srv := newTestTLSServer(t)
+
+ newRole := func(name string, version string, spec types.RoleSpecV6) types.Role {
+ role, err := types.NewRoleWithVersion(name, version, spec)
+ meta := role.GetMetadata()
+ role.SetMetadata(meta)
+ require.NoError(t, err)
+ return role
+ }
+
+ enabledRole := newRole("test_role_enabled", types.V7, types.RoleSpecV6{
+ Allow: types.RoleConditions{
+ Rules: []types.Rule{
+ types.NewRule(types.KindRole, services.RW()),
+ },
+ },
+ Options: types.RoleOptions{
+ SSHPortForwarding: &types.SSHPortForwarding{
+ Remote: &types.SSHRemotePortForwarding{
+ Enabled: types.NewBoolOption(true),
+ },
+ Local: &types.SSHLocalPortForwarding{
+ Enabled: types.NewBoolOption(true),
+ },
+ },
+ },
+ })
+
+ disabledRole := newRole("test_role_disabled", types.V7, types.RoleSpecV6{
+ Allow: types.RoleConditions{
+ Rules: []types.Rule{
+ types.NewRule(types.KindRole, services.RW()),
+ },
+ },
+ Options: types.RoleOptions{
+ SSHPortForwarding: &types.SSHPortForwarding{
+ Remote: &types.SSHRemotePortForwarding{
+ Enabled: types.NewBoolOption(false),
+ },
+ Local: &types.SSHLocalPortForwarding{
+ Enabled: types.NewBoolOption(false),
+ },
+ },
+ },
+ })
+
+ undefinedRole := newRole("test_role_implicit", types.V7, types.RoleSpecV6{
+ Allow: types.RoleConditions{
+ Rules: []types.Rule{
+ types.NewRule(types.KindRole, services.RW()),
+ },
+ },
+ })
+
+ user, err := CreateUser(context.Background(), srv.Auth(), "user", enabledRole, disabledRole, undefinedRole)
+ require.NoError(t, err)
+
+ client, err := srv.NewClient(TestUser(user.GetName()))
+ require.NoError(t, err)
+
+ for _, tc := range []struct {
+ desc string
+ clientVersions []string
+ expectError bool
+ inputRole types.Role
+ expectedRole types.Role
+ expectDowngraded bool
+ }{
+ {
+ desc: "up to date - enabled",
+ clientVersions: []string{
+ "17.1.0", "17.1.0-dev", "",
+ },
+ inputRole: enabledRole,
+ expectedRole: enabledRole,
+ },
+ {
+ desc: "up to date - disabled",
+ clientVersions: []string{
+ "17.1.0", "17.1.0-dev", "",
+ },
+ inputRole: disabledRole,
+ expectedRole: disabledRole,
+ },
+ {
+ desc: "up to date - undefined",
+ clientVersions: []string{
+ "17.1.0", "17.1.0-dev", "",
+ },
+ inputRole: undefinedRole,
+ expectedRole: undefinedRole,
+ },
+ {
+ desc: "downgrade SSH access control granularity - enabled",
+ clientVersions: []string{
+ "17.0.0",
+ },
+ inputRole: enabledRole,
+ expectedRole: newRole(enabledRole.GetName(), types.V7, types.RoleSpecV6{
+ Allow: types.RoleConditions{
+ Rules: []types.Rule{
+ types.NewRule(types.KindRole, services.RW()),
+ },
+ },
+ Options: types.RoleOptions{
+ PortForwarding: types.NewBoolOption(true),
+ SSHPortForwarding: enabledRole.GetOptions().SSHPortForwarding,
+ },
+ }),
+ expectDowngraded: true,
+ },
+ {
+ desc: "downgrade SSH access control granularity - disabled",
+ clientVersions: []string{
+ "17.0.0",
+ },
+ inputRole: disabledRole,
+ expectedRole: newRole(disabledRole.GetName(), types.V7, types.RoleSpecV6{
+ Allow: types.RoleConditions{
+ Rules: []types.Rule{
+ types.NewRule(types.KindRole, services.RW()),
+ },
+ },
+ Options: types.RoleOptions{
+ PortForwarding: types.NewBoolOption(false),
+ SSHPortForwarding: disabledRole.GetOptions().SSHPortForwarding,
+ },
+ }),
+ expectDowngraded: true,
+ },
+ {
+ desc: "downgrade SSH access control granularity - undefined",
+ clientVersions: []string{
+ "17.0.0",
+ },
+ inputRole: undefinedRole,
+ expectedRole: undefinedRole,
+ expectDowngraded: false,
+ },
+ } {
+ t.Run(tc.desc, func(t *testing.T) {
+ for _, clientVersion := range tc.clientVersions {
+ t.Run(clientVersion, func(t *testing.T) {
+ // setup client metadata
+ ctx := context.Background()
+ if clientVersion == "" {
+ ctx = context.WithValue(ctx, metadata.DisableInterceptors{}, struct{}{})
+ } else {
+ ctx = metadata.AddMetadataToContext(ctx, map[string]string{
+ metadata.VersionKey: clientVersion,
+ })
+ }
+
+ checkRole := func(gotRole types.Role) {
+ t.Helper()
+ if tc.expectError {
+ return
+ }
+ require.Empty(t, cmp.Diff(tc.expectedRole, gotRole,
+ cmpopts.IgnoreFields(types.Metadata{}, "Revision", "Labels")))
+ // The downgraded label value won't match exactly because it
+ // includes the client version, so just check it's not empty
+ // and ignore it in the role diff.
+ if tc.expectDowngraded {
+ require.NotEmpty(t, gotRole.GetMetadata().Labels[types.TeleportDowngradedLabel])
+ }
+ }
+ checkErr := func(err error) {
+ t.Helper()
+ if tc.expectError {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ }
+ }
+
+ // Test GetRole
+ gotRole, err := client.GetRole(ctx, tc.inputRole.GetName())
+ checkErr(err)
+ checkRole(gotRole)
+
+ // Test GetRoles
+ gotRoles, err := client.GetRoles(ctx)
+ checkErr(err)
+ if !tc.expectError {
+ foundTestRole := false
+ for _, gotRole := range gotRoles {
+ if gotRole.GetName() != tc.inputRole.GetName() {
+ continue
+ }
+ checkRole(gotRole)
+ foundTestRole = true
+ break
+ }
+ require.True(t, foundTestRole, "GetRoles result does not include expected role")
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
+ defer cancel()
+
+ // Test WatchEvents
+ watcher, err := client.NewWatcher(ctx, types.Watch{Name: "roles", Kinds: []types.WatchKind{{Kind: types.KindRole}}})
+ require.NoError(t, err)
+ defer watcher.Close()
+
+ // Swallow the init event
+ e := <-watcher.Events()
+ require.Equal(t, types.OpInit, e.Type)
+
+ // Re-upsert the role so that the watcher sees it, do this
+ // on the auth server directly to avoid the
+ // TeleportDowngradedLabel check in ServerWithRoles
+ tc.inputRole, err = srv.Auth().UpsertRole(ctx, tc.inputRole)
+ require.NoError(t, err)
+
+ gotRole, err = func() (types.Role, error) {
+ for {
+ select {
+ case <-watcher.Done():
+ return nil, watcher.Error()
+ case e := <-watcher.Events():
+ if gotRole, ok := e.Resource.(types.Role); ok && gotRole.GetName() == tc.inputRole.GetName() {
+ return gotRole, nil
+ }
+ }
+ }
+ }()
+ checkErr(err)
+ checkRole(gotRole)
+
+ if !tc.expectError {
+ // Try to re-upsert the role we got. If it was
+ // downgraded, it should be rejected due to the
+ // TeleportDowngradedLabel
+ _, err = client.UpsertRole(ctx, gotRole)
+ if tc.expectDowngraded {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ }
+ }
+ })
+ }
+ })
+ }
+}
+
func TestUpsertApplicationServerOrigin(t *testing.T) {
t.Parallel()
diff --git a/lib/autoupdate/rollout/strategy_timebased.go b/lib/autoupdate/rollout/strategy_timebased.go
new file mode 100644
index 0000000000000..c5abc34be5588
--- /dev/null
+++ b/lib/autoupdate/rollout/strategy_timebased.go
@@ -0,0 +1,110 @@
+/*
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package rollout
+
+import (
+ "context"
+ "log/slog"
+
+ "github.com/gravitational/trace"
+ "github.com/jonboulle/clockwork"
+
+ "github.com/gravitational/teleport/api/gen/proto/go/teleport/autoupdate/v1"
+ update "github.com/gravitational/teleport/api/types/autoupdate"
+)
+
+const (
+ updateReasonInWindow = "in_window"
+ updateReasonOutsideWindow = "outside_window"
+)
+
+type timeBasedStrategy struct {
+ log *slog.Logger
+ clock clockwork.Clock
+}
+
+func (h *timeBasedStrategy) name() string {
+ return update.AgentsStrategyTimeBased
+}
+
+func newTimeBasedStrategy(log *slog.Logger, clock clockwork.Clock) (rolloutStrategy, error) {
+ if log == nil {
+ return nil, trace.BadParameter("missing log")
+ }
+ if clock == nil {
+ return nil, trace.BadParameter("missing clock")
+ }
+ return &timeBasedStrategy{
+ log: log.With("strategy", update.AgentsStrategyTimeBased),
+ clock: clock,
+ }, nil
+}
+
+func (h *timeBasedStrategy) progressRollout(ctx context.Context, groups []*autoupdate.AutoUpdateAgentRolloutStatusGroup) error {
+ now := h.clock.Now()
+ // We always process every group regardless of the order.
+ var errs []error
+ for _, group := range groups {
+ switch group.State {
+ case autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_UNSTARTED,
+ autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_DONE:
+ // We start any group unstarted group in window.
+ // Done groups can transition back to active if they enter their maintenance window again.
+ // Some agents might have missed the previous windows and might expected to try again.
+ shouldBeActive, err := inWindow(group, now)
+ if err != nil {
+ // In time-based rollouts, groups are not dependent.
+ // Failing to transition a group should affect other groups.
+ // We reflect that something went wrong in the status and go to the next group.
+ setGroupState(group, group.State, updateReasonReconcilerError, now)
+ errs = append(errs, err)
+ continue
+ }
+ if shouldBeActive {
+ setGroupState(group, autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE, updateReasonInWindow, now)
+ } else {
+ setGroupState(group, group.State, updateReasonOutsideWindow, now)
+ }
+ case autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ROLLEDBACK:
+ // We don't touch any group that was manually rolled back.
+ // Something happened and we should not try to update again.
+ case autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE:
+ // The group is currently being updated. We check if the maintenance
+ // is over and if we should transition it to the done state
+ shouldBeActive, err := inWindow(group, now)
+ if err != nil {
+ // In time-based rollouts, groups are not dependent.
+ // Failing to transition a group should affect other groups.
+ // We reflect that something went wrong in the status and go to the next group.
+ setGroupState(group, group.State, updateReasonReconcilerError, now)
+ errs = append(errs, err)
+ continue
+ }
+
+ if shouldBeActive {
+ setGroupState(group, autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE, updateReasonInWindow, now)
+ } else {
+ setGroupState(group, autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_DONE, updateReasonOutsideWindow, now)
+ }
+ default:
+ return trace.BadParameter("unknown autoupdate group state: %v", group.State)
+ }
+ }
+ return trace.NewAggregate(errs...)
+}
diff --git a/lib/autoupdate/rollout/strategy_timebased_test.go b/lib/autoupdate/rollout/strategy_timebased_test.go
new file mode 100644
index 0000000000000..91db29d42e469
--- /dev/null
+++ b/lib/autoupdate/rollout/strategy_timebased_test.go
@@ -0,0 +1,314 @@
+/*
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package rollout
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/jonboulle/clockwork"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ "github.com/gravitational/teleport/api/gen/proto/go/teleport/autoupdate/v1"
+ "github.com/gravitational/teleport/lib/utils"
+)
+
+func Test_progressGroupsTimeBased(t *testing.T) {
+ clock := clockwork.NewFakeClockAt(testSunday)
+ log := utils.NewSlogLoggerForTests()
+ strategy, err := newTimeBasedStrategy(log, clock)
+ require.NoError(t, err)
+
+ groupName := "test-group"
+ canStartToday := everyWeekday
+ cannotStartToday := everyWeekdayButSunday
+ lastUpdate := timestamppb.New(clock.Now().Add(-5 * time.Minute))
+ ctx := context.Background()
+
+ tests := []struct {
+ name string
+ initialState []*autoupdate.AutoUpdateAgentRolloutStatusGroup
+ expectedState []*autoupdate.AutoUpdateAgentRolloutStatusGroup
+ }{
+ {
+ name: "unstarted -> unstarted",
+ initialState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{
+ {
+ Name: groupName,
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_UNSTARTED,
+ LastUpdateTime: lastUpdate,
+ LastUpdateReason: updateReasonCreated,
+ ConfigDays: cannotStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ },
+ expectedState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{
+ {
+ Name: groupName,
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_UNSTARTED,
+ LastUpdateTime: timestamppb.New(clock.Now()),
+ LastUpdateReason: updateReasonOutsideWindow,
+ ConfigDays: cannotStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ },
+ },
+ {
+ name: "unstarted -> active",
+ initialState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{
+ {
+ Name: groupName,
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_UNSTARTED,
+ LastUpdateTime: lastUpdate,
+ LastUpdateReason: updateReasonCreated,
+ ConfigDays: canStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ },
+ expectedState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{
+ {
+ Name: groupName,
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE,
+ StartTime: timestamppb.New(clock.Now()),
+ LastUpdateTime: timestamppb.New(clock.Now()),
+ LastUpdateReason: updateReasonInWindow,
+ ConfigDays: canStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ },
+ },
+ {
+ name: "done -> done",
+ initialState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{
+ {
+ Name: groupName,
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_DONE,
+ LastUpdateTime: lastUpdate,
+ LastUpdateReason: updateReasonOutsideWindow,
+ ConfigDays: cannotStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ },
+ expectedState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{
+ {
+ Name: groupName,
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_DONE,
+ LastUpdateTime: lastUpdate,
+ LastUpdateReason: updateReasonOutsideWindow,
+ ConfigDays: cannotStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ },
+ },
+ {
+ name: "done -> active",
+ initialState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{
+ {
+ Name: groupName,
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_DONE,
+ LastUpdateTime: lastUpdate,
+ StartTime: lastUpdate,
+ LastUpdateReason: updateReasonOutsideWindow,
+ ConfigDays: canStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ },
+ expectedState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{
+ {
+ Name: groupName,
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE,
+ StartTime: timestamppb.New(clock.Now()),
+ LastUpdateTime: timestamppb.New(clock.Now()),
+ LastUpdateReason: updateReasonInWindow,
+ ConfigDays: canStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ },
+ },
+ {
+ name: "active -> active",
+ initialState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{
+ {
+ Name: groupName,
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE,
+ StartTime: lastUpdate,
+ LastUpdateTime: timestamppb.New(clock.Now()),
+ LastUpdateReason: updateReasonInWindow,
+ ConfigDays: canStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ },
+ expectedState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{
+ {
+ Name: groupName,
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE,
+ StartTime: lastUpdate,
+ LastUpdateTime: timestamppb.New(clock.Now()),
+ LastUpdateReason: updateReasonInWindow,
+ ConfigDays: canStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ },
+ },
+ {
+ name: "active -> done",
+ initialState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{
+ {
+ Name: groupName,
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE,
+ StartTime: lastUpdate,
+ LastUpdateTime: timestamppb.New(clock.Now()),
+ LastUpdateReason: updateReasonInWindow,
+ ConfigDays: cannotStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ },
+ expectedState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{
+ {
+ Name: groupName,
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_DONE,
+ StartTime: lastUpdate,
+ LastUpdateTime: timestamppb.New(clock.Now()),
+ LastUpdateReason: updateReasonOutsideWindow,
+ ConfigDays: cannotStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ },
+ },
+ {
+ name: "rolledback is a dead end",
+ initialState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{
+ {
+ Name: groupName + "-in-maintenance",
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ROLLEDBACK,
+ LastUpdateTime: lastUpdate,
+ ConfigDays: canStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ {
+ Name: groupName + "-out-of-maintenance",
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ROLLEDBACK,
+ LastUpdateTime: lastUpdate,
+ ConfigDays: cannotStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ },
+ expectedState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{
+ {
+ Name: groupName + "-in-maintenance",
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ROLLEDBACK,
+ LastUpdateTime: lastUpdate,
+ ConfigDays: canStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ {
+ Name: groupName + "-out-of-maintenance",
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ROLLEDBACK,
+ LastUpdateTime: lastUpdate,
+ ConfigDays: cannotStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ },
+ },
+ {
+ name: "mix of everything",
+ initialState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{
+ {
+ Name: "new group should start",
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_UNSTARTED,
+ LastUpdateTime: lastUpdate,
+ ConfigDays: canStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ {
+ Name: "done group should start",
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_DONE,
+ LastUpdateTime: lastUpdate,
+ StartTime: lastUpdate,
+ ConfigDays: canStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ {
+ Name: "rolledback group should do nothing",
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ROLLEDBACK,
+ LastUpdateTime: lastUpdate,
+ ConfigDays: canStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ {
+ Name: "old group should stop",
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE,
+ LastUpdateTime: lastUpdate,
+ StartTime: lastUpdate,
+ ConfigDays: cannotStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ },
+ expectedState: []*autoupdate.AutoUpdateAgentRolloutStatusGroup{
+ {
+ Name: "new group should start",
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE,
+ StartTime: timestamppb.New(clock.Now()),
+ LastUpdateTime: timestamppb.New(clock.Now()),
+ LastUpdateReason: updateReasonInWindow,
+ ConfigDays: canStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ {
+ Name: "done group should start",
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ACTIVE,
+ StartTime: timestamppb.New(clock.Now()),
+ LastUpdateTime: timestamppb.New(clock.Now()),
+ LastUpdateReason: updateReasonInWindow,
+ ConfigDays: canStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ {
+ Name: "rolledback group should do nothing",
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_ROLLEDBACK,
+ LastUpdateTime: lastUpdate,
+ ConfigDays: canStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ {
+ Name: "old group should stop",
+ State: autoupdate.AutoUpdateAgentGroupState_AUTO_UPDATE_AGENT_GROUP_STATE_DONE,
+ StartTime: lastUpdate,
+ LastUpdateTime: timestamppb.New(clock.Now()),
+ LastUpdateReason: updateReasonOutsideWindow,
+ ConfigDays: cannotStartToday,
+ ConfigStartHour: matchingStartHour,
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ err := strategy.progressRollout(ctx, tt.initialState)
+ require.NoError(t, err)
+ // We use require.Equal instead of Elements match because group order matters.
+ // It's not super important for time-based, but is crucial for halt-on-error.
+ // So it's better to be more conservative and validate order never changes for
+ // both strategies.
+ require.Equal(t, tt.expectedState, tt.initialState)
+ })
+ }
+}
diff --git a/lib/srv/desktop/rdp/rdpclient/client.go b/lib/srv/desktop/rdp/rdpclient/client.go
index ca8ad121e81f9..c7be6dd702016 100644
--- a/lib/srv/desktop/rdp/rdpclient/client.go
+++ b/lib/srv/desktop/rdp/rdpclient/client.go
@@ -291,6 +291,12 @@ func (c *Client) startRustRDP(ctx context.Context) error {
return trace.Wrap(err)
}
+ // [username] need only be valid for the duration of
+ // C.client_run. It is copied on the Rust side and
+ // thus can be freed here.
+ username := C.CString(c.username)
+ defer C.free(unsafe.Pointer(username))
+
// [addr] need only be valid for the duration of
// C.client_run. It is copied on the Rust side and
// thus can be freed here.
@@ -328,6 +334,7 @@ func (c *Client) startRustRDP(ctx context.Context) error {
C.CGOConnectParams{
ad: C.bool(c.cfg.AD),
nla: C.bool(c.cfg.NLA),
+ go_username: username,
go_addr: addr,
go_computer_name: computerName,
go_kdc_addr: kdcAddr,
diff --git a/lib/srv/desktop/rdp/rdpclient/src/client.rs b/lib/srv/desktop/rdp/rdpclient/src/client.rs
index d4e010c8e1fa9..3dae0fc453b59 100644
--- a/lib/srv/desktop/rdp/rdpclient/src/client.rs
+++ b/lib/srv/desktop/rdp/rdpclient/src/client.rs
@@ -44,6 +44,7 @@ use ironrdp_pdu::input::fast_path::{
};
use ironrdp_pdu::input::mouse::PointerFlags;
use ironrdp_pdu::input::{InputEventError, MousePdu};
+use ironrdp_pdu::nego::NegoRequestData;
use ironrdp_pdu::rdp::capability_sets::MajorPlatformType;
use ironrdp_pdu::rdp::client_info::PerformanceFlags;
use ironrdp_pdu::rdp::RdpError;
@@ -1442,8 +1443,11 @@ fn create_config(params: &ConnectParams, pin: String) -> Config {
platform: MajorPlatformType::UNSPECIFIED,
no_server_pointer: false,
autologon: true,
- request_data: None,
pointer_software_rendering: false,
+ // Send the username in the request cookie, which is sent in the initial connection request.
+ // The RDP server ignores this value, but load balancers sitting in front of the server
+ // can use it to implement persistence.
+ request_data: Some(NegoRequestData::cookie(params.username.clone())),
performance_flags: PerformanceFlags::default()
| PerformanceFlags::DISABLE_CURSOR_SHADOW // this is required for pointer to work correctly in Windows 2019
| if !params.show_desktop_wallpaper {
@@ -1457,6 +1461,7 @@ fn create_config(params: &ConnectParams, pin: String) -> Config {
#[derive(Debug)]
pub struct ConnectParams {
+ pub username: String,
pub addr: String,
pub kdc_addr: Option,
pub computer_name: Option,
diff --git a/lib/srv/desktop/rdp/rdpclient/src/lib.rs b/lib/srv/desktop/rdp/rdpclient/src/lib.rs
index 2c79722e73af0..200f1b6a6186a 100644
--- a/lib/srv/desktop/rdp/rdpclient/src/lib.rs
+++ b/lib/srv/desktop/rdp/rdpclient/src/lib.rs
@@ -92,6 +92,7 @@ pub unsafe extern "C" fn free_string(ptr: *mut c_char) {
pub unsafe extern "C" fn client_run(cgo_handle: CgoHandle, params: CGOConnectParams) -> CGOResult {
trace!("client_run");
// Convert from C to Rust types.
+ let username = from_c_string(params.go_username);
let addr = from_c_string(params.go_addr);
let cert_der = from_go_array(params.cert_der, params.cert_der_len);
let key_der = from_go_array(params.key_der, params.key_der_len);
@@ -111,6 +112,7 @@ pub unsafe extern "C" fn client_run(cgo_handle: CgoHandle, params: CGOConnectPar
ConnectParams {
ad: params.ad,
nla: params.nla,
+ username,
addr,
computer_name,
cert_der,
@@ -480,6 +482,7 @@ pub unsafe extern "C" fn client_write_screen_resize(
pub struct CGOConnectParams {
ad: bool,
nla: bool,
+ go_username: *const c_char,
go_addr: *const c_char,
go_domain: *const c_char,
go_kdc_addr: *const c_char,
diff --git a/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx b/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx
index 35cfac569c462..a751566765c27 100644
--- a/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx
+++ b/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import { Flex, LabelInput, Text } from 'design';
import { IconTooltip } from 'design/Tooltip';
diff --git a/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationReview.tsx b/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationReview.tsx
index 426cb1ca81729..39c2c77c0a954 100644
--- a/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationReview.tsx
+++ b/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationReview.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import { Flex, Text } from 'design';
import { IconTooltip } from 'design/Tooltip';
diff --git a/web/packages/shared/components/AccessRequests/AssumeStartTime/AssumeStartTime.story.tsx b/web/packages/shared/components/AccessRequests/AssumeStartTime/AssumeStartTime.story.tsx
index f33c43f4892b8..d20ea9527bc00 100644
--- a/web/packages/shared/components/AccessRequests/AssumeStartTime/AssumeStartTime.story.tsx
+++ b/web/packages/shared/components/AccessRequests/AssumeStartTime/AssumeStartTime.story.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import React, { useState } from 'react';
+import { useState } from 'react';
import { Box, Text } from 'design';
diff --git a/web/packages/shared/components/AccessRequests/NewRequest/CheckableOption.tsx b/web/packages/shared/components/AccessRequests/NewRequest/CheckableOption.tsx
index 510e889ba6c61..46b29b8333b26 100644
--- a/web/packages/shared/components/AccessRequests/NewRequest/CheckableOption.tsx
+++ b/web/packages/shared/components/AccessRequests/NewRequest/CheckableOption.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import { Flex, Text } from 'design';
import { components, OptionProps } from 'react-select';
diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx
index f27c721e77914..2d6f89e019c02 100644
--- a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx
+++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import React, { useState } from 'react';
+import { useState } from 'react';
import { Flex, Text, ButtonIcon, Box, LabelInput } from 'design';
import * as Icon from 'design/Icon';
diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.story.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.story.tsx
index 88d76f0183fab..55ef62d05b61a 100644
--- a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.story.tsx
+++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.story.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import React, { useState } from 'react';
+import { useState } from 'react';
import { MemoryRouter, Link } from 'react-router-dom';
import { Box, ButtonPrimary, ButtonText } from 'design';
diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/SelectReviewers.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/SelectReviewers.tsx
index aedade1435fe7..b1ea398954d48 100644
--- a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/SelectReviewers.tsx
+++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/SelectReviewers.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import React, { useState, useRef } from 'react';
+import { useEffect, useState, useRef } from 'react';
import { components } from 'react-select';
import ReactSelectCreatable from 'react-select/creatable';
import styled from 'styled-components';
@@ -40,7 +40,7 @@ export function SelectReviewers({
() => reviewers.map(r => ({ value: r, label: r, isDisabled: true }))
);
- React.useEffect(() => {
+ useEffect(() => {
// When editing reviewers, auto focus on input box.
if (editReviewers) {
reactSelectRef.current.focus();
diff --git a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestDelete/RequestDelete.story.tsx b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestDelete/RequestDelete.story.tsx
index 52482cc5f1274..2e947ab2b8dfd 100644
--- a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestDelete/RequestDelete.story.tsx
+++ b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestDelete/RequestDelete.story.tsx
@@ -16,8 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
-
import {
makeEmptyAttempt,
makeProcessingAttempt,
diff --git a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestDelete/RequestDelete.tsx b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestDelete/RequestDelete.tsx
index 447da3448da94..8870401c429c6 100644
--- a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestDelete/RequestDelete.tsx
+++ b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestDelete/RequestDelete.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import { ButtonWarning, ButtonSecondary, Flex, Alert } from 'design';
import TextSelectCopy from 'teleport/components/TextSelectCopy';
diff --git a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.story.tsx b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.story.tsx
index 7348178391c47..f755aa1106e31 100644
--- a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.story.tsx
+++ b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.story.tsx
@@ -16,8 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
-
import {
makeSuccessAttempt,
makeEmptyAttempt,
diff --git a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.story.tsx b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.story.tsx
index 5880a1f5c94ee..3feab1d9a952c 100644
--- a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.story.tsx
+++ b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.story.tsx
@@ -16,8 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
-
import {
makeSuccessAttempt,
makeEmptyAttempt,
diff --git a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.tsx b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.tsx
index 9a3e424787378..e6655f64668ac 100644
--- a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.tsx
+++ b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import React from 'react';
+import { Fragment } from 'react';
import styled from 'styled-components';
import {
Alert,
@@ -617,7 +617,7 @@ function Reviews({ reviews }: { reviews: AccessRequestReview[] }) {
review;
return (
-
+
)}
-
+
);
});
diff --git a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RolesRequested.tsx b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RolesRequested.tsx
index 40716141f6339..ccdeb3bcd6c1b 100644
--- a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RolesRequested.tsx
+++ b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RolesRequested.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import { Box, Label } from 'design';
export default function RolesRequested({ roles }: { roles: string[] }) {
diff --git a/web/packages/shared/components/AccessRequests/Shared/Shared.tsx b/web/packages/shared/components/AccessRequests/Shared/Shared.tsx
index 2159f6309c95e..eb8b9ef0bf86a 100644
--- a/web/packages/shared/components/AccessRequests/Shared/Shared.tsx
+++ b/web/packages/shared/components/AccessRequests/Shared/Shared.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import React, { useState } from 'react';
+import { useState } from 'react';
import { ButtonPrimary, Text, Box, ButtonIcon, Menu } from 'design';
import { Info } from 'design/Icon';
import { displayDateWithPrefixedTime } from 'design/datetime';
diff --git a/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.story.tsx b/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.story.tsx
index b2efff7f98c3c..0ce3d0dd52b98 100644
--- a/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.story.tsx
+++ b/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.story.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import React, { useState } from 'react';
+import { useState } from 'react';
import { AdvancedSearchToggle } from './AdvancedSearchToggle';
diff --git a/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.tsx b/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.tsx
index ea37fe2de003d..36a607dbbc5ea 100644
--- a/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.tsx
+++ b/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.tsx
@@ -16,8 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
-
import { Text, Toggle, Link, Flex, H2 } from 'design';
import { P } from 'design/Text/Text';
diff --git a/web/packages/shared/components/AnimatedTerminal/AnimatedTerminal.tsx b/web/packages/shared/components/AnimatedTerminal/AnimatedTerminal.tsx
index d2923a8ae5029..3a320d93dc248 100644
--- a/web/packages/shared/components/AnimatedTerminal/AnimatedTerminal.tsx
+++ b/web/packages/shared/components/AnimatedTerminal/AnimatedTerminal.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import React, { useEffect, useMemo, useRef, useState } from 'react';
+import { useEffect, useMemo, useRef, useState } from 'react';
import {
KeywordHighlight,
diff --git a/web/packages/shared/components/AnimatedTerminal/TerminalContent.tsx b/web/packages/shared/components/AnimatedTerminal/TerminalContent.tsx
index b025bb8e0e80e..5e231094578ea 100644
--- a/web/packages/shared/components/AnimatedTerminal/TerminalContent.tsx
+++ b/web/packages/shared/components/AnimatedTerminal/TerminalContent.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import React, { useEffect, useLayoutEffect, useRef } from 'react';
+import { Fragment, useEffect, useLayoutEffect, useRef } from 'react';
import styled from 'styled-components';
import { BufferEntry } from 'shared/components/AnimatedTerminal/content';
@@ -125,14 +125,14 @@ function renderLines(lines: BufferEntry[], highlights?: KeywordHighlight[]) {
}
const result = lines.map(line => (
-
+
{line.isCommand ? (
${line.text.length > 0 ? ' ' : ''}
) : null}
{formatText(line.text, line.isCommand, highlights)}
{line.isCurrent && line.isCommand ? : null}
-
+
));
return result;
@@ -193,7 +193,7 @@ function formatText(
outer: for (const [index, word] of words.entries()) {
if (!isCommand && /(https?:\/\/\S+)/g.test(word)) {
result.push(
-
+
{word}
{' '}
-
+
);
continue;
diff --git a/web/packages/shared/components/ButtonSso/ButtonSso.story.tsx b/web/packages/shared/components/ButtonSso/ButtonSso.story.tsx
index 7d87e3963bb0c..52a6de6d1c41d 100644
--- a/web/packages/shared/components/ButtonSso/ButtonSso.story.tsx
+++ b/web/packages/shared/components/ButtonSso/ButtonSso.story.tsx
@@ -16,8 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
-
import ButtonSso from './ButtonSso';
export default {
diff --git a/web/packages/shared/components/ButtonSso/ButtonSso.test.tsx b/web/packages/shared/components/ButtonSso/ButtonSso.test.tsx
index fb337001d13b8..aefdc8d5360ba 100644
--- a/web/packages/shared/components/ButtonSso/ButtonSso.test.tsx
+++ b/web/packages/shared/components/ButtonSso/ButtonSso.test.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import { screen } from '@testing-library/react';
import { render } from 'design/utils/testing';
diff --git a/web/packages/shared/components/ButtonSso/ButtonSso.tsx b/web/packages/shared/components/ButtonSso/ButtonSso.tsx
index 9cc4d8c208660..349128a9a1c95 100644
--- a/web/packages/shared/components/ButtonSso/ButtonSso.tsx
+++ b/web/packages/shared/components/ButtonSso/ButtonSso.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import React, { forwardRef } from 'react';
+import { forwardRef } from 'react';
import * as Icons from 'design/Icon';
import { ButtonProps, ButtonSecondary } from 'design/Button';
diff --git a/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.story.tsx b/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.story.tsx
index 23ffd4ff43a3b..6036e570303b3 100644
--- a/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.story.tsx
+++ b/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.story.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import React, { useState } from 'react';
+import { useState } from 'react';
import Flex from 'design/Flex';
import { ButtonTextWithAddIcon } from './ButtonTextWithAddIcon';
diff --git a/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.test.tsx b/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.test.tsx
index 6d88f3c1d26b5..06d24ef84b182 100644
--- a/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.test.tsx
+++ b/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.test.tsx
@@ -16,8 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
-
import { render, fireEvent, screen } from 'design/utils/testing';
import { ButtonTextWithAddIcon } from './ButtonTextWithAddIcon';
diff --git a/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.tsx b/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.tsx
index d025dbbceacbe..3d65b59944f06 100644
--- a/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.tsx
+++ b/web/packages/shared/components/ButtonTextWithAddIcon/ButtonTextWithAddIcon.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import { ButtonText } from 'design';
import { Add as AddIcon } from 'design/Icon';
diff --git a/web/packages/shared/components/ClusterDropdown/ClusterDropdown.story.tsx b/web/packages/shared/components/ClusterDropdown/ClusterDropdown.story.tsx
index 1a26d46287c49..b982926874c1d 100644
--- a/web/packages/shared/components/ClusterDropdown/ClusterDropdown.story.tsx
+++ b/web/packages/shared/components/ClusterDropdown/ClusterDropdown.story.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import { MemoryRouter } from 'react-router';
import { Cluster } from 'teleport/services/clusters';
diff --git a/web/packages/shared/components/Controls/ViewModeSwitch.tsx b/web/packages/shared/components/Controls/ViewModeSwitch.tsx
index 62e5f94b36a3a..2cacc5cdf1d90 100644
--- a/web/packages/shared/components/Controls/ViewModeSwitch.tsx
+++ b/web/packages/shared/components/Controls/ViewModeSwitch.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import styled from 'styled-components';
import { Rows, SquaresFour } from 'design/Icon';
diff --git a/web/packages/shared/components/DownloadConnect/DownloadConnect.story.tsx b/web/packages/shared/components/DownloadConnect/DownloadConnect.story.tsx
index 9758176f55156..fa206cb57b914 100644
--- a/web/packages/shared/components/DownloadConnect/DownloadConnect.story.tsx
+++ b/web/packages/shared/components/DownloadConnect/DownloadConnect.story.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import { Box, Text } from 'design';
import { Platform } from 'design/platform';
diff --git a/web/packages/shared/components/Editor/Tabs.tsx b/web/packages/shared/components/Editor/Tabs.tsx
index 80c43c890e7a5..582e973f78c69 100644
--- a/web/packages/shared/components/Editor/Tabs.tsx
+++ b/web/packages/shared/components/Editor/Tabs.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import styled from 'styled-components';
import * as Icons from 'design/Icon';
diff --git a/web/packages/shared/components/FieldCheckbox/FieldCheckbox.story.tsx b/web/packages/shared/components/FieldCheckbox/FieldCheckbox.story.tsx
index fd646a956a7e2..3c940aeb61a0c 100644
--- a/web/packages/shared/components/FieldCheckbox/FieldCheckbox.story.tsx
+++ b/web/packages/shared/components/FieldCheckbox/FieldCheckbox.story.tsx
@@ -16,8 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
-
import Box from 'design/Box';
import { FieldCheckbox } from '.';
diff --git a/web/packages/shared/components/FieldInput/FieldInput.story.tsx b/web/packages/shared/components/FieldInput/FieldInput.story.tsx
index 1cbcef38cc66e..8de1116cf6f31 100644
--- a/web/packages/shared/components/FieldInput/FieldInput.story.tsx
+++ b/web/packages/shared/components/FieldInput/FieldInput.story.tsx
@@ -16,8 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
-
import { ButtonPrimary, Text } from 'design';
import { EmailSolid } from 'design/Icon';
diff --git a/web/packages/shared/components/FieldInput/FieldInput.test.tsx b/web/packages/shared/components/FieldInput/FieldInput.test.tsx
index 064ca9f612bf9..2440f5587659a 100644
--- a/web/packages/shared/components/FieldInput/FieldInput.test.tsx
+++ b/web/packages/shared/components/FieldInput/FieldInput.test.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import { screen } from '@testing-library/react';
import { render, fireEvent } from 'design/utils/testing';
diff --git a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.story.tsx b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.story.tsx
index 2f798d4d923d1..565f1769ace84 100644
--- a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.story.tsx
+++ b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.story.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import React, { useState } from 'react';
+import { useState } from 'react';
import Box from 'design/Box';
diff --git a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.test.tsx b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.test.tsx
index 89b191e1e5b2d..4d10c34449f26 100644
--- a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.test.tsx
+++ b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.test.tsx
@@ -17,7 +17,7 @@
*/
import userEvent from '@testing-library/user-event';
-import React, { useState } from 'react';
+import { useState } from 'react';
import { act, render, screen } from 'design/utils/testing';
diff --git a/web/packages/shared/components/FieldSelect/FieldSelect.test.tsx b/web/packages/shared/components/FieldSelect/FieldSelect.test.tsx
index 08d9a384b05d3..2c3f1cd829fbd 100644
--- a/web/packages/shared/components/FieldSelect/FieldSelect.test.tsx
+++ b/web/packages/shared/components/FieldSelect/FieldSelect.test.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import { screen } from '@testing-library/react';
import { render, fireEvent } from 'design/utils/testing';
diff --git a/web/packages/shared/components/FieldSelect/FieldSelectCreatable.test.tsx b/web/packages/shared/components/FieldSelect/FieldSelectCreatable.test.tsx
index 13a5ae39089ea..a2f013ad13254 100644
--- a/web/packages/shared/components/FieldSelect/FieldSelectCreatable.test.tsx
+++ b/web/packages/shared/components/FieldSelect/FieldSelectCreatable.test.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import { screen } from '@testing-library/react';
import { render } from 'design/utils/testing';
diff --git a/web/packages/shared/components/FieldTextArea/FieldTextArea.story.tsx b/web/packages/shared/components/FieldTextArea/FieldTextArea.story.tsx
index bdfc4534ba585..ac63d6eee4373 100644
--- a/web/packages/shared/components/FieldTextArea/FieldTextArea.story.tsx
+++ b/web/packages/shared/components/FieldTextArea/FieldTextArea.story.tsx
@@ -16,8 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
-
import { ButtonPrimary, Text } from 'design';
import Validation from '../../components/Validation';
diff --git a/web/packages/shared/components/FileTransfer/FileTransfer.test.tsx b/web/packages/shared/components/FileTransfer/FileTransfer.test.tsx
index 11de431965aad..fbc7caaed47a4 100644
--- a/web/packages/shared/components/FileTransfer/FileTransfer.test.tsx
+++ b/web/packages/shared/components/FileTransfer/FileTransfer.test.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import {
act,
fireEvent,
diff --git a/web/packages/shared/components/FileTransfer/FileTransfer.tsx b/web/packages/shared/components/FileTransfer/FileTransfer.tsx
index a192461b61447..72d88a8dcc0fc 100644
--- a/web/packages/shared/components/FileTransfer/FileTransfer.tsx
+++ b/web/packages/shared/components/FileTransfer/FileTransfer.tsx
@@ -16,8 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
-
import { useFileTransferContext } from './FileTransferContextProvider';
import {
FileTransferDialogDirection,
diff --git a/web/packages/shared/components/FileTransfer/FileTransferActionBar.tsx b/web/packages/shared/components/FileTransfer/FileTransferActionBar.tsx
index 74c0616f8add0..ccb61201fca83 100644
--- a/web/packages/shared/components/FileTransfer/FileTransferActionBar.tsx
+++ b/web/packages/shared/components/FileTransfer/FileTransferActionBar.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import { Flex, ButtonIcon, Text } from 'design';
import * as Icons from 'design/Icon';
import { HoverTooltip } from 'design/Tooltip';
diff --git a/web/packages/shared/components/FileTransfer/FileTransferContextProvider.tsx b/web/packages/shared/components/FileTransfer/FileTransferContextProvider.tsx
index 031d4bbb0608c..bf804472cc17d 100644
--- a/web/packages/shared/components/FileTransfer/FileTransferContextProvider.tsx
+++ b/web/packages/shared/components/FileTransfer/FileTransferContextProvider.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import React, {
+import {
useContext,
useState,
FC,
diff --git a/web/packages/shared/components/FileTransfer/FileTransferRequests.story.tsx b/web/packages/shared/components/FileTransfer/FileTransferRequests.story.tsx
index 72c33603237b7..d36e79d7ea9f0 100644
--- a/web/packages/shared/components/FileTransfer/FileTransferRequests.story.tsx
+++ b/web/packages/shared/components/FileTransfer/FileTransferRequests.story.tsx
@@ -16,8 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
-
import ConsoleContextProvider from 'teleport/Console/consoleContextProvider';
import ConsoleContext from 'teleport/Console/consoleContext';
import { FileTransferRequest } from 'teleport/Console/DocumentSsh/useFileTransfer';
diff --git a/web/packages/shared/components/FileTransfer/FileTransferRequests.tsx b/web/packages/shared/components/FileTransfer/FileTransferRequests.tsx
index 16e864ef7a16b..e526e7c945c44 100644
--- a/web/packages/shared/components/FileTransfer/FileTransferRequests.tsx
+++ b/web/packages/shared/components/FileTransfer/FileTransferRequests.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import styled from 'styled-components';
import { ButtonBorder, Box, Flex, Text, Button } from 'design';
import * as Icons from 'design/Icon';
diff --git a/web/packages/shared/components/FileTransfer/FileTransferStateless/DownloadForm/DownloadForm.test.tsx b/web/packages/shared/components/FileTransfer/FileTransferStateless/DownloadForm/DownloadForm.test.tsx
index be91459bbd712..be88e37eb1c57 100644
--- a/web/packages/shared/components/FileTransfer/FileTransferStateless/DownloadForm/DownloadForm.test.tsx
+++ b/web/packages/shared/components/FileTransfer/FileTransferStateless/DownloadForm/DownloadForm.test.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import { fireEvent, render, screen } from 'design/utils/testing';
import { DownloadForm } from './DownloadForm';
diff --git a/web/packages/shared/components/FileTransfer/FileTransferStateless/DownloadForm/DownloadForm.tsx b/web/packages/shared/components/FileTransfer/FileTransferStateless/DownloadForm/DownloadForm.tsx
index f0b457dbf4447..d428d76f4d09d 100644
--- a/web/packages/shared/components/FileTransfer/FileTransferStateless/DownloadForm/DownloadForm.tsx
+++ b/web/packages/shared/components/FileTransfer/FileTransferStateless/DownloadForm/DownloadForm.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import React, { useId, useState } from 'react';
+import { useId, useState } from 'react';
import { Flex, LabelInput } from 'design';
import { ButtonPrimary } from 'design/Button';
diff --git a/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileList.test.tsx b/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileList.test.tsx
index 9e1c64d6e8b73..ebc4d8fc620cc 100644
--- a/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileList.test.tsx
+++ b/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileList.test.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import { fireEvent, render, screen } from 'design/utils/testing';
import { TransferredFile } from '../types';
diff --git a/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileList.tsx b/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileList.tsx
index 052e9e100a86e..305be469d9bc4 100644
--- a/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileList.tsx
+++ b/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileList.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import styled from 'styled-components';
import { TransferredFile } from '../types';
diff --git a/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileListItem.tsx b/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileListItem.tsx
index f55a27612a0bd..9c048535de51a 100644
--- a/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileListItem.tsx
+++ b/web/packages/shared/components/FileTransfer/FileTransferStateless/FileList/FileListItem.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import React, { FC, PropsWithChildren, useEffect } from 'react';
+import { FC, PropsWithChildren, useEffect } from 'react';
import styled from 'styled-components';
import { ButtonIcon, Flex, Text } from 'design';
import { CircleCheck, Cross, Warning } from 'design/Icon';
diff --git a/web/packages/shared/components/FileTransfer/FileTransferStateless/FileTransferStateless.story.tsx b/web/packages/shared/components/FileTransfer/FileTransferStateless/FileTransferStateless.story.tsx
index c2a212ea68b4d..b7ae036ad3070 100644
--- a/web/packages/shared/components/FileTransfer/FileTransferStateless/FileTransferStateless.story.tsx
+++ b/web/packages/shared/components/FileTransfer/FileTransferStateless/FileTransferStateless.story.tsx
@@ -16,8 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
-
import { FileTransferContainer } from '../FileTransferContainer';
import {
diff --git a/web/packages/shared/components/FileTransfer/FileTransferStateless/FileTransferStateless.tsx b/web/packages/shared/components/FileTransfer/FileTransferStateless/FileTransferStateless.tsx
index a67f6ac03de16..1d9eea8d995f7 100644
--- a/web/packages/shared/components/FileTransfer/FileTransferStateless/FileTransferStateless.tsx
+++ b/web/packages/shared/components/FileTransfer/FileTransferStateless/FileTransferStateless.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import styled from 'styled-components';
import { ButtonIcon, Flex, Text } from 'design';
import { Cross as CloseIcon } from 'design/Icon';
diff --git a/web/packages/shared/components/FileTransfer/FileTransferStateless/UploadForm/UploadForm.test.tsx b/web/packages/shared/components/FileTransfer/FileTransferStateless/UploadForm/UploadForm.test.tsx
index 465387081d940..4493b575717fe 100644
--- a/web/packages/shared/components/FileTransfer/FileTransferStateless/UploadForm/UploadForm.test.tsx
+++ b/web/packages/shared/components/FileTransfer/FileTransferStateless/UploadForm/UploadForm.test.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import { fireEvent, render, screen } from 'design/utils/testing';
import { UploadForm } from './UploadForm';
diff --git a/web/packages/shared/components/Highlight/Highlight.story.tsx b/web/packages/shared/components/Highlight/Highlight.story.tsx
index 946dc1f512ddf..46a1817087d88 100644
--- a/web/packages/shared/components/Highlight/Highlight.story.tsx
+++ b/web/packages/shared/components/Highlight/Highlight.story.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import styled from 'styled-components';
import { Flex, Text } from 'design';
diff --git a/web/packages/shared/components/Highlight/Highlight.tsx b/web/packages/shared/components/Highlight/Highlight.tsx
index 017c1903729c4..c1a3c73767471 100644
--- a/web/packages/shared/components/Highlight/Highlight.tsx
+++ b/web/packages/shared/components/Highlight/Highlight.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import { findAll } from 'highlight-words-core';
/**
diff --git a/web/packages/shared/components/MenuAction/MenuAction.story.tsx b/web/packages/shared/components/MenuAction/MenuAction.story.tsx
index 156193ea6aeac..4b5bbc4a5ed1e 100644
--- a/web/packages/shared/components/MenuAction/MenuAction.story.tsx
+++ b/web/packages/shared/components/MenuAction/MenuAction.story.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import { Flex } from 'design';
import { Cog } from 'design/Icon';
diff --git a/web/packages/shared/components/MenuAction/MenuAction.test.tsx b/web/packages/shared/components/MenuAction/MenuAction.test.tsx
index 184bb434e495a..e376c2ecf98dc 100644
--- a/web/packages/shared/components/MenuAction/MenuAction.test.tsx
+++ b/web/packages/shared/components/MenuAction/MenuAction.test.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import { screen } from '@testing-library/react';
import { render, fireEvent } from 'design/utils/testing';
diff --git a/web/packages/shared/components/MenuLogin/MenuLogin.test.tsx b/web/packages/shared/components/MenuLogin/MenuLogin.test.tsx
index 585fc04b8b54e..fec647363bb23 100644
--- a/web/packages/shared/components/MenuLogin/MenuLogin.test.tsx
+++ b/web/packages/shared/components/MenuLogin/MenuLogin.test.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import { render, fireEvent, screen, waitFor } from 'design/utils/testing';
import { MenuLogin } from './MenuLogin';
diff --git a/web/packages/shared/components/Notification/Notification.story.tsx b/web/packages/shared/components/Notification/Notification.story.tsx
index 4df7b2e7ee299..b3b2b1b9fe5ad 100644
--- a/web/packages/shared/components/Notification/Notification.story.tsx
+++ b/web/packages/shared/components/Notification/Notification.story.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import React, { useState } from 'react';
+import { useState } from 'react';
import { Bots } from 'design/Icon';
import Flex from 'design/Flex';
diff --git a/web/packages/shared/components/Search/SearchPagination.tsx b/web/packages/shared/components/Search/SearchPagination.tsx
index d4d31851abf7d..f2cdbe89ec7e6 100644
--- a/web/packages/shared/components/Search/SearchPagination.tsx
+++ b/web/packages/shared/components/Search/SearchPagination.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import { Flex } from 'design';
import { StyledPanel } from 'design/DataTable/StyledTable';
import { StyledArrowBtn } from 'design/DataTable/Pager/StyledPager';
diff --git a/web/packages/shared/components/Search/SearchPanel.tsx b/web/packages/shared/components/Search/SearchPanel.tsx
index e4dec55a30776..4c470b6b790b9 100644
--- a/web/packages/shared/components/Search/SearchPanel.tsx
+++ b/web/packages/shared/components/Search/SearchPanel.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import React, { useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
import styled from 'styled-components';
import { Flex } from 'design';
import InputSearch from 'design/DataTable/InputSearch';
diff --git a/web/packages/shared/components/Select/Select.tsx b/web/packages/shared/components/Select/Select.tsx
index 2f86a5d87a0cf..1beaab055b905 100644
--- a/web/packages/shared/components/Select/Select.tsx
+++ b/web/packages/shared/components/Select/Select.tsx
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-import React from 'react';
import ReactSelect, {
ClearIndicatorProps,
DropdownIndicatorProps,
diff --git a/web/packages/shared/components/Select/SelectCreatable.story.tsx b/web/packages/shared/components/Select/SelectCreatable.story.tsx
index 02bfcba2cc647..e813e6f4496cd 100644
--- a/web/packages/shared/components/Select/SelectCreatable.story.tsx
+++ b/web/packages/shared/components/Select/SelectCreatable.story.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import React from 'react';
+import { useState } from 'react';
import { Flex, Box } from 'design';
import { SelectCreatable, Option } from '../Select';
@@ -26,10 +26,10 @@ export default {
};
export const Selects = () => {
- const [input, setInput] = React.useState('');
- const [inputMulti, setInputMulti] = React.useState('');
- const [selected, setSelected] = React.useState