diff --git a/api/gen/proto/go/userpreferences/v1/unified_resource_preferences.pb.go b/api/gen/proto/go/userpreferences/v1/unified_resource_preferences.pb.go
new file mode 100644
index 0000000000000..4b2839b40b414
--- /dev/null
+++ b/api/gen/proto/go/userpreferences/v1/unified_resource_preferences.pb.go
@@ -0,0 +1,232 @@
+// Copyright 2023 Gravitational, Inc
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.31.0
+// protoc (unknown)
+// source: teleport/userpreferences/v1/unified_resource_preferences.proto
+
+package userpreferencesv1
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// DefaultTab is the default tab selected in the unified resource web UI
+type DefaultTab int32
+
+const (
+ DefaultTab_DEFAULT_TAB_UNSPECIFIED DefaultTab = 0
+ // ALL is all resources
+ DefaultTab_DEFAULT_TAB_ALL DefaultTab = 1
+ // PINNED is only pinned resources
+ DefaultTab_DEFAULT_TAB_PINNED DefaultTab = 2
+)
+
+// Enum value maps for DefaultTab.
+var (
+ DefaultTab_name = map[int32]string{
+ 0: "DEFAULT_TAB_UNSPECIFIED",
+ 1: "DEFAULT_TAB_ALL",
+ 2: "DEFAULT_TAB_PINNED",
+ }
+ DefaultTab_value = map[string]int32{
+ "DEFAULT_TAB_UNSPECIFIED": 0,
+ "DEFAULT_TAB_ALL": 1,
+ "DEFAULT_TAB_PINNED": 2,
+ }
+)
+
+func (x DefaultTab) Enum() *DefaultTab {
+ p := new(DefaultTab)
+ *p = x
+ return p
+}
+
+func (x DefaultTab) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (DefaultTab) Descriptor() protoreflect.EnumDescriptor {
+ return file_teleport_userpreferences_v1_unified_resource_preferences_proto_enumTypes[0].Descriptor()
+}
+
+func (DefaultTab) Type() protoreflect.EnumType {
+ return &file_teleport_userpreferences_v1_unified_resource_preferences_proto_enumTypes[0]
+}
+
+func (x DefaultTab) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use DefaultTab.Descriptor instead.
+func (DefaultTab) EnumDescriptor() ([]byte, []int) {
+ return file_teleport_userpreferences_v1_unified_resource_preferences_proto_rawDescGZIP(), []int{0}
+}
+
+// UnifiedResourcePreferences are preferences used in the Unified Resource web UI
+type UnifiedResourcePreferences struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ // default_tab is the default tab selected in the unified resource web UI
+ DefaultTab DefaultTab `protobuf:"varint,1,opt,name=default_tab,json=defaultTab,proto3,enum=teleport.userpreferences.v1.DefaultTab" json:"default_tab,omitempty"`
+}
+
+func (x *UnifiedResourcePreferences) Reset() {
+ *x = UnifiedResourcePreferences{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_teleport_userpreferences_v1_unified_resource_preferences_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *UnifiedResourcePreferences) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*UnifiedResourcePreferences) ProtoMessage() {}
+
+func (x *UnifiedResourcePreferences) ProtoReflect() protoreflect.Message {
+ mi := &file_teleport_userpreferences_v1_unified_resource_preferences_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use UnifiedResourcePreferences.ProtoReflect.Descriptor instead.
+func (*UnifiedResourcePreferences) Descriptor() ([]byte, []int) {
+ return file_teleport_userpreferences_v1_unified_resource_preferences_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *UnifiedResourcePreferences) GetDefaultTab() DefaultTab {
+ if x != nil {
+ return x.DefaultTab
+ }
+ return DefaultTab_DEFAULT_TAB_UNSPECIFIED
+}
+
+var File_teleport_userpreferences_v1_unified_resource_preferences_proto protoreflect.FileDescriptor
+
+var file_teleport_userpreferences_v1_unified_resource_preferences_proto_rawDesc = []byte{
+ 0x0a, 0x3e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x70,
+ 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x6e,
+ 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70,
+ 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x12, 0x1b, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x70,
+ 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x22, 0x66, 0x0a,
+ 0x1a, 0x55, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
+ 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x48, 0x0a, 0x0b, 0x64,
+ 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x74, 0x61, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e,
+ 0x32, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x75, 0x73, 0x65, 0x72,
+ 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44,
+ 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x54, 0x61, 0x62, 0x52, 0x0a, 0x64, 0x65, 0x66, 0x61, 0x75,
+ 0x6c, 0x74, 0x54, 0x61, 0x62, 0x2a, 0x56, 0x0a, 0x0a, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74,
+ 0x54, 0x61, 0x62, 0x12, 0x1b, 0x0a, 0x17, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x5f, 0x54,
+ 0x41, 0x42, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00,
+ 0x12, 0x13, 0x0a, 0x0f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x5f, 0x54, 0x41, 0x42, 0x5f,
+ 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54,
+ 0x5f, 0x54, 0x41, 0x42, 0x5f, 0x50, 0x49, 0x4e, 0x4e, 0x45, 0x44, 0x10, 0x02, 0x42, 0x59, 0x5a,
+ 0x57, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x76,
+ 0x69, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f,
+ 0x72, 0x74, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x2f, 0x67, 0x6f, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e,
+ 0x63, 0x65, 0x73, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65,
+ 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_teleport_userpreferences_v1_unified_resource_preferences_proto_rawDescOnce sync.Once
+ file_teleport_userpreferences_v1_unified_resource_preferences_proto_rawDescData = file_teleport_userpreferences_v1_unified_resource_preferences_proto_rawDesc
+)
+
+func file_teleport_userpreferences_v1_unified_resource_preferences_proto_rawDescGZIP() []byte {
+ file_teleport_userpreferences_v1_unified_resource_preferences_proto_rawDescOnce.Do(func() {
+ file_teleport_userpreferences_v1_unified_resource_preferences_proto_rawDescData = protoimpl.X.CompressGZIP(file_teleport_userpreferences_v1_unified_resource_preferences_proto_rawDescData)
+ })
+ return file_teleport_userpreferences_v1_unified_resource_preferences_proto_rawDescData
+}
+
+var file_teleport_userpreferences_v1_unified_resource_preferences_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_teleport_userpreferences_v1_unified_resource_preferences_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_teleport_userpreferences_v1_unified_resource_preferences_proto_goTypes = []interface{}{
+ (DefaultTab)(0), // 0: teleport.userpreferences.v1.DefaultTab
+ (*UnifiedResourcePreferences)(nil), // 1: teleport.userpreferences.v1.UnifiedResourcePreferences
+}
+var file_teleport_userpreferences_v1_unified_resource_preferences_proto_depIdxs = []int32{
+ 0, // 0: teleport.userpreferences.v1.UnifiedResourcePreferences.default_tab:type_name -> teleport.userpreferences.v1.DefaultTab
+ 1, // [1:1] is the sub-list for method output_type
+ 1, // [1:1] is the sub-list for method input_type
+ 1, // [1:1] is the sub-list for extension type_name
+ 1, // [1:1] is the sub-list for extension extendee
+ 0, // [0:1] is the sub-list for field type_name
+}
+
+func init() { file_teleport_userpreferences_v1_unified_resource_preferences_proto_init() }
+func file_teleport_userpreferences_v1_unified_resource_preferences_proto_init() {
+ if File_teleport_userpreferences_v1_unified_resource_preferences_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_teleport_userpreferences_v1_unified_resource_preferences_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*UnifiedResourcePreferences); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_teleport_userpreferences_v1_unified_resource_preferences_proto_rawDesc,
+ NumEnums: 1,
+ NumMessages: 1,
+ NumExtensions: 0,
+ NumServices: 0,
+ },
+ GoTypes: file_teleport_userpreferences_v1_unified_resource_preferences_proto_goTypes,
+ DependencyIndexes: file_teleport_userpreferences_v1_unified_resource_preferences_proto_depIdxs,
+ EnumInfos: file_teleport_userpreferences_v1_unified_resource_preferences_proto_enumTypes,
+ MessageInfos: file_teleport_userpreferences_v1_unified_resource_preferences_proto_msgTypes,
+ }.Build()
+ File_teleport_userpreferences_v1_unified_resource_preferences_proto = out.File
+ file_teleport_userpreferences_v1_unified_resource_preferences_proto_rawDesc = nil
+ file_teleport_userpreferences_v1_unified_resource_preferences_proto_goTypes = nil
+ file_teleport_userpreferences_v1_unified_resource_preferences_proto_depIdxs = nil
+}
diff --git a/api/gen/proto/go/userpreferences/v1/userpreferences.pb.go b/api/gen/proto/go/userpreferences/v1/userpreferences.pb.go
index 49635d3d8e266..2c9bb1a573c1b 100644
--- a/api/gen/proto/go/userpreferences/v1/userpreferences.pb.go
+++ b/api/gen/proto/go/userpreferences/v1/userpreferences.pb.go
@@ -49,6 +49,8 @@ type UserPreferences struct {
Onboard *OnboardUserPreferences `protobuf:"bytes,3,opt,name=onboard,proto3" json:"onboard,omitempty"`
// cluster_preferences are user preferences saved per cluster.
ClusterPreferences *ClusterUserPreferences `protobuf:"bytes,4,opt,name=cluster_preferences,json=clusterPreferences,proto3" json:"cluster_preferences,omitempty"`
+ // unified_resource_preferences are user preferences saved for the Unified Resource web UI
+ UnifiedResourcePreferences *UnifiedResourcePreferences `protobuf:"bytes,5,opt,name=unified_resource_preferences,json=unifiedResourcePreferences,proto3" json:"unified_resource_preferences,omitempty"`
}
func (x *UserPreferences) Reset() {
@@ -111,6 +113,13 @@ func (x *UserPreferences) GetClusterPreferences() *ClusterUserPreferences {
return nil
}
+func (x *UserPreferences) GetUnifiedResourcePreferences() *UnifiedResourcePreferences {
+ if x != nil {
+ return x.UnifiedResourcePreferences
+ }
+ return nil
+}
+
// GetUserPreferencesRequest is a request to get the user preferences.
type GetUserPreferencesRequest struct {
state protoimpl.MessageState
@@ -269,7 +278,11 @@ var file_teleport_userpreferences_v1_userpreferences_proto_rawDesc = []byte{
0x61, 0x72, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x27, 0x74, 0x65, 0x6c, 0x65, 0x70,
0x6f, 0x72, 0x74, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e,
0x63, 0x65, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x68, 0x65, 0x6d, 0x65, 0x2e, 0x70, 0x72, 0x6f,
- 0x74, 0x6f, 0x22, 0xcc, 0x02, 0x0a, 0x0f, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65,
+ 0x74, 0x6f, 0x1a, 0x3e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x75, 0x73, 0x65,
+ 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2f, 0x76, 0x31, 0x2f,
+ 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
+ 0x5f, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f,
+ 0x74, 0x6f, 0x22, 0xc7, 0x03, 0x0a, 0x0f, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65,
0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x4a, 0x0a, 0x06, 0x61, 0x73, 0x73, 0x69, 0x73, 0x74,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72,
0x74, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65,
@@ -290,47 +303,55 @@ var file_teleport_userpreferences_v1_userpreferences_proto_rawDesc = []byte{
0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x55, 0x73,
0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x12, 0x63,
0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65,
- 0x73, 0x22, 0x2b, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66,
- 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4a, 0x04,
- 0x08, 0x01, 0x10, 0x02, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x6c,
- 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65,
- 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x0b,
- 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28,
- 0x0b, 0x32, 0x2c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x75, 0x73, 0x65,
- 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e,
+ 0x73, 0x12, 0x79, 0x0a, 0x1c, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x73,
+ 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65,
+ 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f,
+ 0x72, 0x74, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63,
+ 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x52, 0x65, 0x73,
+ 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73,
+ 0x52, 0x1a, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
+ 0x65, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x22, 0x2b, 0x0a, 0x19,
+ 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63,
+ 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x52,
+ 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x6c, 0x0a, 0x1a, 0x47, 0x65, 0x74,
0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52,
- 0x0b, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x22, 0x7e, 0x0a, 0x1c,
- 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72,
- 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4e, 0x0a, 0x0b,
- 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28,
- 0x0b, 0x32, 0x2c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x75, 0x73, 0x65,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x0b, 0x70, 0x72, 0x65, 0x66, 0x65,
+ 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x74,
+ 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66,
+ 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x50,
+ 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x0b, 0x70, 0x72, 0x65, 0x66,
+ 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x22, 0x7e, 0x0a, 0x1c, 0x55, 0x70, 0x73, 0x65, 0x72,
+ 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4e, 0x0a, 0x0b, 0x70, 0x72, 0x65, 0x66, 0x65,
+ 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x74,
+ 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66,
+ 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x50,
+ 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x0b, 0x70, 0x72, 0x65, 0x66,
+ 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x52, 0x08, 0x75,
+ 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x32, 0x8c, 0x02, 0x0a, 0x16, 0x55, 0x73, 0x65, 0x72,
+ 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69,
+ 0x63, 0x65, 0x12, 0x85, 0x01, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72,
+ 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x36, 0x2e, 0x74, 0x65, 0x6c, 0x65,
+ 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65,
+ 0x6e, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50,
+ 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x1a, 0x37, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x75, 0x73, 0x65,
0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e,
- 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52,
- 0x0b, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x4a, 0x04, 0x08, 0x02,
- 0x10, 0x03, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x32, 0x8c, 0x02, 0x0a,
- 0x16, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73,
- 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x85, 0x01, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x55,
- 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x36,
- 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x70, 0x72,
- 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74,
- 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52,
- 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72,
- 0x74, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65,
- 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66,
- 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
- 0x6a, 0x0a, 0x15, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65,
- 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x39, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70,
- 0x6f, 0x72, 0x74, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e,
- 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, 0x65,
- 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75,
- 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
- 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x59, 0x5a, 0x57, 0x67,
- 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x76, 0x69, 0x74,
- 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74,
- 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67,
- 0x6f, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65,
- 0x73, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65,
- 0x6e, 0x63, 0x65, 0x73, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+ 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63,
+ 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x15, 0x55, 0x70,
+ 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e,
+ 0x63, 0x65, 0x73, 0x12, 0x39, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x75,
+ 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2e, 0x76,
+ 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66,
+ 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16,
+ 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
+ 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x59, 0x5a, 0x57, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
+ 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x76, 0x69, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e,
+ 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x61, 0x70, 0x69, 0x2f,
+ 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x2f, 0x75, 0x73, 0x65,
+ 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2f, 0x76, 0x31, 0x3b,
+ 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x76,
+ 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -355,24 +376,26 @@ var file_teleport_userpreferences_v1_userpreferences_proto_goTypes = []interface
(Theme)(0), // 5: teleport.userpreferences.v1.Theme
(*OnboardUserPreferences)(nil), // 6: teleport.userpreferences.v1.OnboardUserPreferences
(*ClusterUserPreferences)(nil), // 7: teleport.userpreferences.v1.ClusterUserPreferences
- (*emptypb.Empty)(nil), // 8: google.protobuf.Empty
+ (*UnifiedResourcePreferences)(nil), // 8: teleport.userpreferences.v1.UnifiedResourcePreferences
+ (*emptypb.Empty)(nil), // 9: google.protobuf.Empty
}
var file_teleport_userpreferences_v1_userpreferences_proto_depIdxs = []int32{
4, // 0: teleport.userpreferences.v1.UserPreferences.assist:type_name -> teleport.userpreferences.v1.AssistUserPreferences
5, // 1: teleport.userpreferences.v1.UserPreferences.theme:type_name -> teleport.userpreferences.v1.Theme
6, // 2: teleport.userpreferences.v1.UserPreferences.onboard:type_name -> teleport.userpreferences.v1.OnboardUserPreferences
7, // 3: teleport.userpreferences.v1.UserPreferences.cluster_preferences:type_name -> teleport.userpreferences.v1.ClusterUserPreferences
- 0, // 4: teleport.userpreferences.v1.GetUserPreferencesResponse.preferences:type_name -> teleport.userpreferences.v1.UserPreferences
- 0, // 5: teleport.userpreferences.v1.UpsertUserPreferencesRequest.preferences:type_name -> teleport.userpreferences.v1.UserPreferences
- 1, // 6: teleport.userpreferences.v1.UserPreferencesService.GetUserPreferences:input_type -> teleport.userpreferences.v1.GetUserPreferencesRequest
- 3, // 7: teleport.userpreferences.v1.UserPreferencesService.UpsertUserPreferences:input_type -> teleport.userpreferences.v1.UpsertUserPreferencesRequest
- 2, // 8: teleport.userpreferences.v1.UserPreferencesService.GetUserPreferences:output_type -> teleport.userpreferences.v1.GetUserPreferencesResponse
- 8, // 9: teleport.userpreferences.v1.UserPreferencesService.UpsertUserPreferences:output_type -> google.protobuf.Empty
- 8, // [8:10] is the sub-list for method output_type
- 6, // [6:8] is the sub-list for method input_type
- 6, // [6:6] is the sub-list for extension type_name
- 6, // [6:6] is the sub-list for extension extendee
- 0, // [0:6] is the sub-list for field type_name
+ 8, // 4: teleport.userpreferences.v1.UserPreferences.unified_resource_preferences:type_name -> teleport.userpreferences.v1.UnifiedResourcePreferences
+ 0, // 5: teleport.userpreferences.v1.GetUserPreferencesResponse.preferences:type_name -> teleport.userpreferences.v1.UserPreferences
+ 0, // 6: teleport.userpreferences.v1.UpsertUserPreferencesRequest.preferences:type_name -> teleport.userpreferences.v1.UserPreferences
+ 1, // 7: teleport.userpreferences.v1.UserPreferencesService.GetUserPreferences:input_type -> teleport.userpreferences.v1.GetUserPreferencesRequest
+ 3, // 8: teleport.userpreferences.v1.UserPreferencesService.UpsertUserPreferences:input_type -> teleport.userpreferences.v1.UpsertUserPreferencesRequest
+ 2, // 9: teleport.userpreferences.v1.UserPreferencesService.GetUserPreferences:output_type -> teleport.userpreferences.v1.GetUserPreferencesResponse
+ 9, // 10: teleport.userpreferences.v1.UserPreferencesService.UpsertUserPreferences:output_type -> google.protobuf.Empty
+ 9, // [9:11] is the sub-list for method output_type
+ 7, // [7:9] is the sub-list for method input_type
+ 7, // [7:7] is the sub-list for extension type_name
+ 7, // [7:7] is the sub-list for extension extendee
+ 0, // [0:7] is the sub-list for field type_name
}
func init() { file_teleport_userpreferences_v1_userpreferences_proto_init() }
@@ -384,6 +407,7 @@ func file_teleport_userpreferences_v1_userpreferences_proto_init() {
file_teleport_userpreferences_v1_cluster_preferences_proto_init()
file_teleport_userpreferences_v1_onboard_proto_init()
file_teleport_userpreferences_v1_theme_proto_init()
+ file_teleport_userpreferences_v1_unified_resource_preferences_proto_init()
if !protoimpl.UnsafeEnabled {
file_teleport_userpreferences_v1_userpreferences_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*UserPreferences); i {
diff --git a/api/proto/teleport/userpreferences/v1/unified_resource_preferences.proto b/api/proto/teleport/userpreferences/v1/unified_resource_preferences.proto
new file mode 100644
index 0000000000000..7b66ede935fa9
--- /dev/null
+++ b/api/proto/teleport/userpreferences/v1/unified_resource_preferences.proto
@@ -0,0 +1,34 @@
+// Copyright 2023 Gravitational, Inc
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package teleport.userpreferences.v1;
+
+option go_package = "github.com/gravitational/teleport/api/gen/proto/go/userpreferences/v1;userpreferencesv1";
+
+// UnifiedResourcePreferences are preferences used in the Unified Resource web UI
+message UnifiedResourcePreferences {
+ // default_tab is the default tab selected in the unified resource web UI
+ DefaultTab default_tab = 1;
+}
+
+// DefaultTab is the default tab selected in the unified resource web UI
+enum DefaultTab {
+ DEFAULT_TAB_UNSPECIFIED = 0;
+ // ALL is all resources
+ DEFAULT_TAB_ALL = 1;
+ // PINNED is only pinned resources
+ DEFAULT_TAB_PINNED = 2;
+}
diff --git a/api/proto/teleport/userpreferences/v1/userpreferences.proto b/api/proto/teleport/userpreferences/v1/userpreferences.proto
index 452121b654273..46d0f57b46394 100644
--- a/api/proto/teleport/userpreferences/v1/userpreferences.proto
+++ b/api/proto/teleport/userpreferences/v1/userpreferences.proto
@@ -21,6 +21,7 @@ import "teleport/userpreferences/v1/assist.proto";
import "teleport/userpreferences/v1/cluster_preferences.proto";
import "teleport/userpreferences/v1/onboard.proto";
import "teleport/userpreferences/v1/theme.proto";
+import "teleport/userpreferences/v1/unified_resource_preferences.proto";
option go_package = "github.com/gravitational/teleport/api/gen/proto/go/userpreferences/v1;userpreferencesv1";
@@ -34,6 +35,8 @@ message UserPreferences {
v1.OnboardUserPreferences onboard = 3;
// cluster_preferences are user preferences saved per cluster.
v1.ClusterUserPreferences cluster_preferences = 4;
+ // unified_resource_preferences are user preferences saved for the Unified Resource web UI
+ UnifiedResourcePreferences unified_resource_preferences = 5;
}
// GetUserPreferencesRequest is a request to get the user preferences.
diff --git a/lib/auth/userpreferences/userpreferencesv1/service_test.go b/lib/auth/userpreferences/userpreferencesv1/service_test.go
index ff2092f64a6dc..0d1f3f5c3dfcf 100644
--- a/lib/auth/userpreferences/userpreferencesv1/service_test.go
+++ b/lib/auth/userpreferences/userpreferencesv1/service_test.go
@@ -58,6 +58,9 @@ func TestService_GetUserPreferences(t *testing.T) {
ViewMode: userpreferencesv1.AssistViewMode_ASSIST_VIEW_MODE_DOCKED,
},
Theme: userpreferencesv1.Theme_THEME_LIGHT,
+ UnifiedResourcePreferences: &userpreferencesv1.UnifiedResourcePreferences{
+ DefaultTab: userpreferencesv1.DefaultTab_DEFAULT_TAB_ALL,
+ },
Onboard: &userpreferencesv1.OnboardUserPreferences{
PreferredResources: []userpreferencesv1.Resource{},
MarketingParams: &userpreferencesv1.MarketingParams{},
diff --git a/lib/services/local/userpreferences.go b/lib/services/local/userpreferences.go
index 75a1777ca9d21..1da1a7d8147fc 100644
--- a/lib/services/local/userpreferences.go
+++ b/lib/services/local/userpreferences.go
@@ -41,6 +41,9 @@ func DefaultUserPreferences() *userpreferencesv1.UserPreferences {
ViewMode: userpreferencesv1.AssistViewMode_ASSIST_VIEW_MODE_DOCKED,
},
Theme: userpreferencesv1.Theme_THEME_LIGHT,
+ UnifiedResourcePreferences: &userpreferencesv1.UnifiedResourcePreferences{
+ DefaultTab: userpreferencesv1.DefaultTab_DEFAULT_TAB_ALL,
+ },
Onboard: &userpreferencesv1.OnboardUserPreferences{
PreferredResources: []userpreferencesv1.Resource{},
MarketingParams: &userpreferencesv1.MarketingParams{},
diff --git a/lib/services/local/userpreferences_test.go b/lib/services/local/userpreferences_test.go
index 965035ceae650..a8436f77937f5 100644
--- a/lib/services/local/userpreferences_test.go
+++ b/lib/services/local/userpreferences_test.go
@@ -99,9 +99,29 @@ func TestUserPreferencesCRUD(t *testing.T) {
},
},
expected: &userpreferencesv1.UserPreferences{
- Assist: defaultPref.Assist,
- Onboard: defaultPref.Onboard,
- Theme: userpreferencesv1.Theme_THEME_DARK,
+ Assist: defaultPref.Assist,
+ Onboard: defaultPref.Onboard,
+ Theme: userpreferencesv1.Theme_THEME_DARK,
+ UnifiedResourcePreferences: defaultPref.UnifiedResourcePreferences,
+ ClusterPreferences: defaultPref.ClusterPreferences,
+ },
+ },
+ {
+ name: "update the unified tab preference only",
+ req: &userpreferencesv1.UpsertUserPreferencesRequest{
+ Preferences: &userpreferencesv1.UserPreferences{
+ UnifiedResourcePreferences: &userpreferencesv1.UnifiedResourcePreferences{
+ DefaultTab: userpreferencesv1.DefaultTab_DEFAULT_TAB_PINNED,
+ },
+ },
+ },
+ expected: &userpreferencesv1.UserPreferences{
+ Assist: defaultPref.Assist,
+ Onboard: defaultPref.Onboard,
+ Theme: defaultPref.Theme,
+ UnifiedResourcePreferences: &userpreferencesv1.UnifiedResourcePreferences{
+ DefaultTab: userpreferencesv1.DefaultTab_DEFAULT_TAB_PINNED,
+ },
ClusterPreferences: defaultPref.ClusterPreferences,
},
},
@@ -119,8 +139,9 @@ func TestUserPreferencesCRUD(t *testing.T) {
},
},
expected: &userpreferencesv1.UserPreferences{
- Theme: defaultPref.Theme,
- Onboard: defaultPref.Onboard,
+ Theme: defaultPref.Theme,
+ UnifiedResourcePreferences: defaultPref.UnifiedResourcePreferences,
+ Onboard: defaultPref.Onboard,
Assist: &userpreferencesv1.AssistUserPreferences{
PreferredLogins: []string{"foo", "bar"},
ViewMode: defaultPref.Assist.ViewMode,
@@ -138,8 +159,9 @@ func TestUserPreferencesCRUD(t *testing.T) {
},
},
expected: &userpreferencesv1.UserPreferences{
- Theme: defaultPref.Theme,
- Onboard: defaultPref.Onboard,
+ Theme: defaultPref.Theme,
+ UnifiedResourcePreferences: defaultPref.UnifiedResourcePreferences,
+ Onboard: defaultPref.Onboard,
Assist: &userpreferencesv1.AssistUserPreferences{
PreferredLogins: defaultPref.Assist.PreferredLogins,
ViewMode: userpreferencesv1.AssistViewMode_ASSIST_VIEW_MODE_POPUP_EXPANDED_SIDEBAR_VISIBLE,
@@ -163,8 +185,9 @@ func TestUserPreferencesCRUD(t *testing.T) {
},
},
expected: &userpreferencesv1.UserPreferences{
- Assist: defaultPref.Assist,
- Theme: defaultPref.Theme,
+ Assist: defaultPref.Assist,
+ Theme: defaultPref.Theme,
+ UnifiedResourcePreferences: defaultPref.UnifiedResourcePreferences,
Onboard: &userpreferencesv1.OnboardUserPreferences{
PreferredResources: []userpreferencesv1.Resource{userpreferencesv1.Resource_RESOURCE_DATABASES},
MarketingParams: &userpreferencesv1.MarketingParams{
@@ -189,9 +212,10 @@ func TestUserPreferencesCRUD(t *testing.T) {
},
},
expected: &userpreferencesv1.UserPreferences{
- Assist: defaultPref.Assist,
- Theme: defaultPref.Theme,
- Onboard: defaultPref.Onboard,
+ Assist: defaultPref.Assist,
+ Theme: defaultPref.Theme,
+ UnifiedResourcePreferences: defaultPref.UnifiedResourcePreferences,
+ Onboard: defaultPref.Onboard,
ClusterPreferences: &userpreferencesv1.ClusterUserPreferences{
PinnedResources: &userpreferencesv1.PinnedResourcesUserPreferences{
ResourceIds: []string{"node1", "node2"},
@@ -204,6 +228,9 @@ func TestUserPreferencesCRUD(t *testing.T) {
req: &userpreferencesv1.UpsertUserPreferencesRequest{
Preferences: &userpreferencesv1.UserPreferences{
Theme: userpreferencesv1.Theme_THEME_LIGHT,
+ UnifiedResourcePreferences: &userpreferencesv1.UnifiedResourcePreferences{
+ DefaultTab: userpreferencesv1.DefaultTab_DEFAULT_TAB_PINNED,
+ },
Assist: &userpreferencesv1.AssistUserPreferences{
PreferredLogins: []string{"baz"},
ViewMode: userpreferencesv1.AssistViewMode_ASSIST_VIEW_MODE_POPUP,
@@ -226,6 +253,9 @@ func TestUserPreferencesCRUD(t *testing.T) {
},
expected: &userpreferencesv1.UserPreferences{
Theme: userpreferencesv1.Theme_THEME_LIGHT,
+ UnifiedResourcePreferences: &userpreferencesv1.UnifiedResourcePreferences{
+ DefaultTab: userpreferencesv1.DefaultTab_DEFAULT_TAB_PINNED,
+ },
Assist: &userpreferencesv1.AssistUserPreferences{
PreferredLogins: []string{"baz"},
ViewMode: userpreferencesv1.AssistViewMode_ASSIST_VIEW_MODE_POPUP,
diff --git a/lib/web/userpreferences.go b/lib/web/userpreferences.go
index f842c59c0875a..9e2113bdb32ef 100644
--- a/lib/web/userpreferences.go
+++ b/lib/web/userpreferences.go
@@ -50,12 +50,17 @@ type ClusterUserPreferencesResponse struct {
PinnedResources []string `json:"pinnedResources"`
}
+type UnifiedResourcePreferencesResponse struct {
+ DefaultTab userpreferencesv1.DefaultTab `json:"defaultTab"`
+}
+
// UserPreferencesResponse is the JSON response for the user preferences.
type UserPreferencesResponse struct {
- Assist AssistUserPreferencesResponse `json:"assist"`
- Theme userpreferencesv1.Theme `json:"theme"`
- Onboard OnboardUserPreferencesResponse `json:"onboard"`
- ClusterPreferences ClusterUserPreferencesResponse `json:"clusterPreferences,omitempty"`
+ Assist AssistUserPreferencesResponse `json:"assist"`
+ Theme userpreferencesv1.Theme `json:"theme"`
+ UnifiedResourcePreferences UnifiedResourcePreferencesResponse `json:"unifiedResourcePreferences"`
+ Onboard OnboardUserPreferencesResponse `json:"onboard"`
+ ClusterPreferences ClusterUserPreferencesResponse `json:"clusterPreferences,omitempty"`
}
func (h *Handler) getUserClusterPreferences(_ http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnelclient.RemoteSite) (interface{}, error) {
@@ -69,7 +74,7 @@ func (h *Handler) getUserClusterPreferences(_ http.ResponseWriter, r *http.Reque
return nil, trace.Wrap(err)
}
- return resp.Preferences.ClusterPreferences, nil
+ return clusterPreferencesResponse(resp.Preferences.ClusterPreferences), nil
}
// updateUserClusterPreferences is a handler for PUT /webapi/user/preferences.
@@ -113,6 +118,9 @@ func makePreferenceRequest(req UserPreferencesResponse) *userpreferencesv1.Upser
return &userpreferencesv1.UpsertUserPreferencesRequest{
Preferences: &userpreferencesv1.UserPreferences{
Theme: req.Theme,
+ UnifiedResourcePreferences: &userpreferencesv1.UnifiedResourcePreferences{
+ DefaultTab: req.UnifiedResourcePreferences.DefaultTab,
+ },
Assist: &userpreferencesv1.AssistUserPreferences{
PreferredLogins: req.Assist.PreferredLogins,
ViewMode: req.Assist.ViewMode,
@@ -159,10 +167,11 @@ func (h *Handler) updateUserPreferences(_ http.ResponseWriter, r *http.Request,
// userPreferencesResponse creates a JSON response for the user preferences.
func userPreferencesResponse(resp *userpreferencesv1.UserPreferences) *UserPreferencesResponse {
jsonResp := &UserPreferencesResponse{
- Assist: assistUserPreferencesResponse(resp.Assist),
- Theme: resp.Theme,
- Onboard: onboardUserPreferencesResponse(resp.Onboard),
- ClusterPreferences: clusterPreferencesResponse(resp.ClusterPreferences),
+ Assist: assistUserPreferencesResponse(resp.Assist),
+ Theme: resp.Theme,
+ Onboard: onboardUserPreferencesResponse(resp.Onboard),
+ ClusterPreferences: clusterPreferencesResponse(resp.ClusterPreferences),
+ UnifiedResourcePreferences: unifiedResourcePreferencesResponse(resp.UnifiedResourcePreferences),
}
return jsonResp
@@ -186,6 +195,13 @@ func assistUserPreferencesResponse(resp *userpreferencesv1.AssistUserPreferences
return jsonResp
}
+// unifiedResourcePreferencesResponse creates a JSON response for the assist user preferences.
+func unifiedResourcePreferencesResponse(resp *userpreferencesv1.UnifiedResourcePreferences) UnifiedResourcePreferencesResponse {
+ return UnifiedResourcePreferencesResponse{
+ DefaultTab: resp.DefaultTab,
+ }
+}
+
// onboardUserPreferencesResponse creates a JSON response for the onboard user preferences.
func onboardUserPreferencesResponse(resp *userpreferencesv1.OnboardUserPreferences) OnboardUserPreferencesResponse {
jsonResp := OnboardUserPreferencesResponse{
diff --git a/web/packages/design/src/Checkbox/Checkbox.tsx b/web/packages/design/src/Checkbox/Checkbox.tsx
index f4525e37f834d..d939e418f2d93 100644
--- a/web/packages/design/src/Checkbox/Checkbox.tsx
+++ b/web/packages/design/src/Checkbox/Checkbox.tsx
@@ -43,3 +43,38 @@ export const CheckboxInput = styled.input`
${space}
`;
+
+// TODO (avatus): Make this the default checkbox
+export const StyledCheckbox = styled.input.attrs({ type: 'checkbox' })`
+ // reset the appearance so we can style the background
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ width: 16px;
+ height: 16px;
+ border: 1px solid ${props => props.theme.colors.text.muted};
+ border-radius: ${props => props.theme.radii[1]}px;
+ background: transparent;
+ position: relative;
+
+ &:checked {
+ border: 1px solid ${props => props.theme.colors.brand};
+ background-color: ${props => props.theme.colors.brand};
+ }
+
+ &:hover {
+ cursor: pointer;
+ }
+
+ &::before {
+ content: '';
+ display: block;
+ }
+
+ &:checked::before {
+ content: '✓';
+ color: ${props => props.theme.colors.levels.deep};
+ position: absolute;
+ right: 1px;
+ }
+`;
diff --git a/web/packages/design/src/Checkbox/index.ts b/web/packages/design/src/Checkbox/index.ts
index a9c2d7725a19f..a14c9ed6e324c 100644
--- a/web/packages/design/src/Checkbox/index.ts
+++ b/web/packages/design/src/Checkbox/index.ts
@@ -14,4 +14,4 @@
* limitations under the License.
*/
-export { CheckboxInput, CheckboxWrapper } from './Checkbox';
+export { CheckboxInput, CheckboxWrapper, StyledCheckbox } from './Checkbox';
diff --git a/web/packages/teleport/src/Apps/useApps.ts b/web/packages/teleport/src/Apps/useApps.ts
index d85ad7f188518..31d37e1c974ab 100644
--- a/web/packages/teleport/src/Apps/useApps.ts
+++ b/web/packages/teleport/src/Apps/useApps.ts
@@ -29,8 +29,10 @@ export function useApps(ctx: Ctx) {
const isEnterprise = ctx.isEnterprise;
const { params, search, ...filteringProps } = useUrlFiltering({
- fieldName: 'name',
- dir: 'ASC',
+ sort: {
+ fieldName: 'name',
+ dir: 'ASC',
+ },
});
const { fetch, ...paginationProps } = useServerSidePagination({
diff --git a/web/packages/teleport/src/Console/DocumentNodes/useNodes.ts b/web/packages/teleport/src/Console/DocumentNodes/useNodes.ts
index 3c5ce734f82c4..3f66ddcb25cb5 100644
--- a/web/packages/teleport/src/Console/DocumentNodes/useNodes.ts
+++ b/web/packages/teleport/src/Console/DocumentNodes/useNodes.ts
@@ -31,8 +31,10 @@ export default function useNodes({ clusterId, id }: stores.DocumentNodes) {
const consoleCtx = useConsoleContext();
const { params, search, ...filteringProps } = useUrlFiltering({
- fieldName: 'hostname',
- dir: 'ASC',
+ sort: {
+ fieldName: 'hostname',
+ dir: 'ASC',
+ },
});
const { fetch, fetchedData, ...paginationProps } = useServerSidePagination({
diff --git a/web/packages/teleport/src/Databases/useDatabases.ts b/web/packages/teleport/src/Databases/useDatabases.ts
index 867765fea5053..dd5436884a7cb 100644
--- a/web/packages/teleport/src/Databases/useDatabases.ts
+++ b/web/packages/teleport/src/Databases/useDatabases.ts
@@ -31,8 +31,10 @@ export function useDatabases(ctx: Ctx) {
const accessRequestId = ctx.storeUser.getAccessRequestId();
const { params, search, ...filteringProps } = useUrlFiltering({
- fieldName: 'name',
- dir: 'ASC',
+ sort: {
+ fieldName: 'name',
+ dir: 'ASC',
+ },
});
const { fetch, ...paginationProps } = useServerSidePagination({
diff --git a/web/packages/teleport/src/Desktops/useDesktops.ts b/web/packages/teleport/src/Desktops/useDesktops.ts
index 0ce9e61b68239..11914ee10212d 100644
--- a/web/packages/teleport/src/Desktops/useDesktops.ts
+++ b/web/packages/teleport/src/Desktops/useDesktops.ts
@@ -35,8 +35,10 @@ export function useDesktops(ctx: Ctx) {
const username = ctx.storeUser.state.username;
const { params, search, ...filteringProps } = useUrlFiltering({
- fieldName: 'name',
- dir: 'ASC',
+ sort: {
+ fieldName: 'name',
+ dir: 'ASC',
+ },
});
const { fetch, ...paginationProps } = useServerSidePagination({
diff --git a/web/packages/teleport/src/Discover/SelectResource/SelectResource.story.tsx b/web/packages/teleport/src/Discover/SelectResource/SelectResource.story.tsx
index c8a5f62091269..b5dce64b68847 100644
--- a/web/packages/teleport/src/Discover/SelectResource/SelectResource.story.tsx
+++ b/web/packages/teleport/src/Discover/SelectResource/SelectResource.story.tsx
@@ -96,6 +96,8 @@ const Provider = ({
}: ProviderProps) => {
const ctx = createTeleportContext({ customAcl: customAcl });
const updatePreferences = () => Promise.resolve();
+ const getClusterPinnedResources = () => Promise.resolve([]);
+ const updateClusterPinnedResources = () => Promise.resolve();
const preferences: UserPreferences = makeDefaultUserPreferences();
preferences.onboard.preferredResources = resources;
@@ -103,7 +105,14 @@ const Provider = ({
-
+
{children}
diff --git a/web/packages/teleport/src/Kubes/useKubes.ts b/web/packages/teleport/src/Kubes/useKubes.ts
index e8bb8e9934fcc..17f19c7299c88 100644
--- a/web/packages/teleport/src/Kubes/useKubes.ts
+++ b/web/packages/teleport/src/Kubes/useKubes.ts
@@ -30,8 +30,10 @@ export function useKubes(ctx: TeleportContext) {
const accessRequestId = ctx.storeUser.getAccessRequestId();
const { params, search, ...filteringProps } = useUrlFiltering({
- fieldName: 'name',
- dir: 'ASC',
+ sort: {
+ fieldName: 'name',
+ dir: 'ASC',
+ },
});
const { fetch, ...paginationProps } = useServerSidePagination({
diff --git a/web/packages/teleport/src/Nodes/useNodes.ts b/web/packages/teleport/src/Nodes/useNodes.ts
index 4525167c19e03..8096690bf7af9 100644
--- a/web/packages/teleport/src/Nodes/useNodes.ts
+++ b/web/packages/teleport/src/Nodes/useNodes.ts
@@ -32,8 +32,10 @@ export function useNodes(ctx: Ctx) {
const canCreate = ctx.storeUser.getTokenAccess().create;
const { params, search, ...filteringProps } = useUrlFiltering({
- fieldName: 'hostname',
- dir: 'ASC',
+ sort: {
+ fieldName: 'hostname',
+ dir: 'ASC',
+ },
});
const { fetch, fetchedData, ...paginationProps } = useServerSidePagination({
diff --git a/web/packages/teleport/src/UnifiedResources/FilterPanel.tsx b/web/packages/teleport/src/UnifiedResources/FilterPanel.tsx
index 0eecbc37fa75d..19388aa82e3f1 100644
--- a/web/packages/teleport/src/UnifiedResources/FilterPanel.tsx
+++ b/web/packages/teleport/src/UnifiedResources/FilterPanel.tsx
@@ -18,15 +18,16 @@ import React, { useState } from 'react';
import styled from 'styled-components';
import { ButtonBorder, ButtonPrimary, ButtonSecondary } from 'design/Button';
import { SortDir } from 'design/DataTable/types';
-import { Text } from 'design';
+import { Text, Flex } from 'design';
import Menu, { MenuItem } from 'design/Menu';
-import Flex from 'design/Flex';
-import { CheckboxInput } from 'design/Checkbox';
+import { StyledCheckbox } from 'design/Checkbox';
import { ArrowUp, ArrowDown, ChevronDown } from 'design/Icon';
import { encodeUrlQueryParams } from 'teleport/components/hooks/useUrlFiltering';
import { ResourceFilter, SortType } from 'teleport/services/agents';
+import { HoverTooltip } from './Resources';
+
const kindOptions = [
{ label: 'Application', value: 'app' },
{ label: 'Database', value: 'db' },
@@ -46,6 +47,9 @@ export interface FilterPanelProps {
params: ResourceFilter;
setParams: (params: ResourceFilter) => void;
setSort: (sort: SortType) => void;
+ selectVisible: () => void;
+ selected: boolean;
+ shouldUnpin: boolean;
}
export function FilterPanel({
@@ -54,6 +58,9 @@ export function FilterPanel({
params,
setParams,
setSort,
+ selectVisible,
+ selected,
+ shouldUnpin,
}: FilterPanelProps) {
const { sort, kinds } = params;
@@ -72,7 +79,8 @@ export function FilterPanel({
params.search ?? params.query,
params.sort,
newKinds,
- isAdvancedSearch
+ isAdvancedSearch,
+ params.pinnedOnly
)
);
};
@@ -86,22 +94,26 @@ export function FilterPanel({
};
return (
-
-
-
-
+
+ {shouldUnpin ? 'Deselect all' : 'Select all'}>}
+ >
+
+
+
+
);
- return null;
}
function oppositeSort(sort: SortType): SortType {
@@ -166,20 +178,23 @@ const FilterTypesMenu = ({
};
return (
-
- props.theme.colors.spotBackground[0]};
- `}
- textTransform="none"
- size="small"
- onClick={handleOpen}
- >
- Types {kindsFromParams.length > 0 ? `(${kindsFromParams.length})` : ''}
-
- {kindsFromParams.length > 0 && }
-
+
+ Filter types>}>
+ props.theme.colors.spotBackground[0]};
+ `}
+ textTransform="none"
+ size="small"
+ onClick={handleOpen}
+ >
+ Types{' '}
+ {kindsFromParams.length > 0 ? `(${kindsFromParams.length})` : ''}
+
+ {kindsFromParams.length > 0 && }
+
+
-
+
+
+ {selectedResources.length > 0 &&
+ (pinningNotSupported ? (
+ {PINNING_NOT_SUPPORTED_MESSAGE}>}>
+ {$pinAllButton}
+
+ ) : (
+ $pinAllButton
+ ))}
+
-
- {resources.map(res => (
-
+ {tabs.map(tab => (
+ selectTab(tab.value)}
+ disabled={
+ tab.value === UnifiedTabPreference.Pinned && pinningNotSupported
+ }
+ title={tab.label}
+ isSelected={
+ params.pinnedOnly
+ ? tab.value === UnifiedTabPreference.Pinned
+ : tab.value === UnifiedTabPreference.All
+ }
/>
))}
- {/* Using index as key here is ok because these elements never change order */}
- {attempt.status === 'processing' &&
- loadingCardArray.map((_, i) => )}
-
+
+ {pinningNotSupported && params.pinnedOnly ? (
+
+ ) : (
+
+ {getPinnedResourcesAttempt.status !== 'processing' &&
+ resources.map(res => {
+ const key = resourceKey(res);
+ return (
+
+ );
+ })}
+ {/* Using index as key here is ok because these elements never change order */}
+ {(attempt.status === 'processing' ||
+ getPinnedResourcesAttempt.status === 'processing') &&
+ loadingCardArray.map((_, i) => (
+
+ ))}
+
+ )}
{attempt.status === 'failed' && resources.length > 0 && (
Load more
)}
- {noResults && isSearchEmpty && (
+ {noResults && isSearchEmpty && !params.pinnedOnly && (
)}
+ {noResults && params.pinnedOnly && isSearchEmpty && }
{noResults && !isSearchEmpty && (
-
+
)}
@@ -183,7 +417,7 @@ export function Resources() {
export function resourceKey(resource: UnifiedResource) {
if (resource.kind === 'node') {
- return `${resource.hostname}/node`;
+ return `${resource.hostname}/${resource.id}/node`;
}
return `${resource.name}/${resource.kind}`;
}
@@ -198,14 +432,36 @@ export function resourceName(resource: UnifiedResource) {
return resource.name;
}
-function NoResults({ query }: { query: string }) {
+function NoPinned() {
+ return (
+
+ You have not pinned any resources
+
+ );
+}
+
+function PinningNotSupported() {
+ return (
+
+ {PINNING_NOT_SUPPORTED_MESSAGE}
+
+ );
+}
+
+function NoResults({
+ query,
+ isPinnedTab,
+}: {
+ query: string;
+ isPinnedTab: boolean;
+}) {
// Prevent `No resources were found for ""` flicker.
if (query) {
return (
- No resources were found for
+ No {isPinnedTab ? 'pinned ' : ''}resources were found for
= ({ tipContent, fontSize = 10, children }) => {
+ const [anchorEl, setAnchorEl] = useState();
+ const open = Boolean(anchorEl);
+
+ function handlePopoverOpen(event) {
+ setAnchorEl(event.currentTarget);
+ }
+
+ function handlePopoverClose() {
+ setAnchorEl(null);
+ }
+
+ return (
+
+ {children}
+
+
+ {tipContent}
+
+
+
+ );
+};
+
+const modalCss = () => `
+ pointer-events: none;
+`;
+
+const StyledOnHover = styled(Text)`
+ color: ${props => props.theme.colors.text.main};
+ background-color: ${props => props.theme.colors.tooltip.background};
+ max-width: 350px;
+`;
diff --git a/web/packages/teleport/src/UnifiedResources/SearchPanel.tsx b/web/packages/teleport/src/UnifiedResources/SearchPanel.tsx
index 8ba080b061417..d10a085b60b46 100644
--- a/web/packages/teleport/src/UnifiedResources/SearchPanel.tsx
+++ b/web/packages/teleport/src/UnifiedResources/SearchPanel.tsx
@@ -47,7 +47,7 @@ export function SearchPanel({
}
return (
-
+
diff --git a/web/packages/teleport/src/User/UserContext.test.tsx b/web/packages/teleport/src/User/UserContext.test.tsx
index 1a1a131455960..7a2b75348ab1e 100644
--- a/web/packages/teleport/src/User/UserContext.test.tsx
+++ b/web/packages/teleport/src/User/UserContext.test.tsx
@@ -18,6 +18,7 @@ import React from 'react';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
+import { MemoryRouter } from 'react-router';
import { render, screen, waitFor } from '@testing-library/react';
@@ -60,9 +61,11 @@ describe('user context - success state', () => {
it('should render with the settings from the backend', async () => {
render(
-
-
-
+
+
+
+
+
);
const theme = await screen.findByText(/theme: light/i);
@@ -84,9 +87,11 @@ describe('user context - success state', () => {
localStorage.setItem(KeysEnum.THEME, 'dark');
render(
-
-
-
+
+
+
+
+
);
await waitFor(() => expect(updateBody.theme).toEqual(ThemePreference.Dark));
@@ -111,9 +116,11 @@ describe('user context - error state', () => {
it('should render with the default settings', async () => {
render(
-
-
-
+
+
+
+
+
);
const theme = await screen.findByText(/theme: light/i);
@@ -125,9 +132,11 @@ describe('user context - error state', () => {
localStorage.setItem(KeysEnum.THEME, 'dark');
render(
-
-
-
+
+
+
+
+
);
const theme = await screen.findByText(/theme: dark/i);
@@ -145,9 +154,11 @@ describe('user context - error state', () => {
);
render(
-
-
-
+
+
+
+
+
);
const theme = await screen.findByText(/theme: dark/i);
diff --git a/web/packages/teleport/src/User/UserContext.tsx b/web/packages/teleport/src/User/UserContext.tsx
index 5fd6832188ac0..305f65998f951 100644
--- a/web/packages/teleport/src/User/UserContext.tsx
+++ b/web/packages/teleport/src/User/UserContext.tsx
@@ -16,8 +16,10 @@
import React, {
createContext,
+ useCallback,
PropsWithChildren,
useContext,
+ useRef,
useEffect,
useState,
} from 'react';
@@ -29,6 +31,7 @@ import { Indicator } from 'design';
import { StyledIndicator } from 'teleport/Main';
import * as service from 'teleport/services/userPreferences';
+import cfg from 'teleport/config';
import storage, { KeysEnum } from 'teleport/services/localStorage';
@@ -39,11 +42,19 @@ import {
import { makeDefaultUserPreferences } from 'teleport/services/userPreferences/userPreferences';
-import type { UserPreferences } from 'teleport/services/userPreferences/types';
+import type {
+ UserClusterPreferences,
+ UserPreferences,
+} from 'teleport/services/userPreferences/types';
export interface UserContextValue {
preferences: UserPreferences;
updatePreferences: (preferences: Partial) => Promise;
+ updateClusterPinnedResources: (
+ clusterId: string,
+ pinnedResources: string[]
+ ) => Promise;
+ getClusterPinnedResources: (clusterId: string) => Promise;
}
export const UserContext = createContext(null);
@@ -54,18 +65,52 @@ export function useUser(): UserContextValue {
export function UserContextProvider(props: PropsWithChildren) {
const { attempt, run } = useAttempt('processing');
+ // because we have to update cluster preferences with itself during the update
+ // we useRef here to prevent infinite rerenders
+ const clusterPreferences = useRef>({});
const [preferences, setPreferences] = useState(
makeDefaultUserPreferences()
);
+ const getClusterPinnedResources = useCallback(async (clusterId: string) => {
+ if (clusterPreferences.current[clusterId]) {
+ // we know that pinned resources is supported because we've already successfully
+ // fetched their pinned resources once before
+ localStorage.removeItem(KeysEnum.PINNED_RESOURCES_NOT_SUPPORTED);
+ return clusterPreferences.current[clusterId].pinnedResources;
+ }
+ const prefs = await service.getUserClusterPreferences(clusterId);
+ if (prefs) {
+ clusterPreferences.current[clusterId] = prefs;
+ return prefs.pinnedResources;
+ }
+ return null;
+ }, []);
+
+ const updateClusterPinnedResources = async (
+ clusterId: string,
+ pinnedResources: string[]
+ ) => {
+ if (!clusterPreferences.current[clusterId]) {
+ clusterPreferences.current[clusterId] = { pinnedResources: [] };
+ }
+ clusterPreferences.current[clusterId].pinnedResources = pinnedResources;
+
+ return service.updateUserClusterPreferences(clusterId, {
+ ...preferences,
+ clusterPreferences: clusterPreferences.current[clusterId],
+ });
+ };
+
async function loadUserPreferences() {
const storedPreferences = storage.getUserPreferences();
const theme = storage.getDeprecatedThemePreference();
try {
const preferences = await service.getUserPreferences();
-
+ clusterPreferences.current[cfg.proxyCluster] =
+ preferences.clusterPreferences;
if (!storedPreferences) {
// there are no mirrored user preferences in local storage so this is the first time
// the user has requested their preferences in this browser session
@@ -113,8 +158,14 @@ export function UserContextProvider(props: PropsWithChildren) {
...preferences.onboard,
...newPreferences.onboard,
},
+ unifiedResourcePreferences: {
+ ...preferences.unifiedResourcePreferences,
+ ...newPreferences.unifiedResourcePreferences,
+ },
+ // updatePreferences only update the root cluster so we can only pass cluster
+ // preferences from the root cluster
+ clusterPreferences: clusterPreferences.current[cfg.proxyCluster],
} as UserPreferences;
-
setPreferences(nextPreferences);
storage.setUserPreferences(nextPreferences);
@@ -148,7 +199,14 @@ export function UserContextProvider(props: PropsWithChildren) {
}
return (
-
+
{props.children}
);
diff --git a/web/packages/teleport/src/User/testHelpers/makeTestUserContext.ts b/web/packages/teleport/src/User/testHelpers/makeTestUserContext.ts
index b189a1e6419aa..bdd64196fa8d7 100644
--- a/web/packages/teleport/src/User/testHelpers/makeTestUserContext.ts
+++ b/web/packages/teleport/src/User/testHelpers/makeTestUserContext.ts
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import { makeEmptyAttempt } from 'shared/hooks/useAsync';
+
import { UserContextValue } from 'teleport/User/UserContext';
export const makeTestUserContext = (
@@ -31,7 +33,13 @@ export const makeTestUserContext = (
preferredResources: [],
},
},
+ clusterPreferences: {
+ pinnedResources: [],
+ },
+ updateClusterPreferencesAttempt: makeEmptyAttempt(),
updatePreferences: () => Promise.resolve(),
+ updateClusterPinnedResources: () => Promise.resolve(),
+ getClusterPinnedResources: () => Promise.resolve(),
},
overrides
);
diff --git a/web/packages/teleport/src/components/ServersideSearchPanel/useServerSideSearchPanel.ts b/web/packages/teleport/src/components/ServersideSearchPanel/useServerSideSearchPanel.ts
index 8853b0ed4bf3c..035042f1adf67 100644
--- a/web/packages/teleport/src/components/ServersideSearchPanel/useServerSideSearchPanel.ts
+++ b/web/packages/teleport/src/components/ServersideSearchPanel/useServerSideSearchPanel.ts
@@ -55,7 +55,8 @@ export default function useServersideSearchPanel({
searchString,
params.sort,
params.kinds,
- isAdvancedSearch
+ isAdvancedSearch,
+ params.pinnedOnly
)
);
}
diff --git a/web/packages/teleport/src/components/hooks/useUrlFiltering/encodeUrlQueryParams.ts b/web/packages/teleport/src/components/hooks/useUrlFiltering/encodeUrlQueryParams.ts
index f87fa2c176b3f..788763a48eef8 100644
--- a/web/packages/teleport/src/components/hooks/useUrlFiltering/encodeUrlQueryParams.ts
+++ b/web/packages/teleport/src/components/hooks/useUrlFiltering/encodeUrlQueryParams.ts
@@ -21,7 +21,8 @@ export function encodeUrlQueryParams(
searchString: string,
sort: SortType | null,
kinds: string[] | null,
- isAdvancedSearch: boolean
+ isAdvancedSearch: boolean,
+ pinnedOnly: boolean
) {
const urlParams = new URLSearchParams();
@@ -33,6 +34,10 @@ export function encodeUrlQueryParams(
urlParams.append('sort', `${sort.fieldName}:${sort.dir.toLowerCase()}`);
}
+ if (pinnedOnly) {
+ urlParams.append('pinnedOnly', `${pinnedOnly}`);
+ }
+
if (kinds) {
for (const kind of kinds) {
urlParams.append('kinds', kind);
diff --git a/web/packages/teleport/src/components/hooks/useUrlFiltering/useUrlFiltering.test.tsx b/web/packages/teleport/src/components/hooks/useUrlFiltering/useUrlFiltering.test.tsx
index 6280380e23560..89040e6b3f780 100644
--- a/web/packages/teleport/src/components/hooks/useUrlFiltering/useUrlFiltering.test.tsx
+++ b/web/packages/teleport/src/components/hooks/useUrlFiltering/useUrlFiltering.test.tsx
@@ -37,7 +37,7 @@ test('extracting params from URL with simple search and sort params', () => {
const history = createMemoryHistory({ initialEntries: [url] });
let result;
- result = renderHook(() => useUrlFiltering(initialSort), {
+ result = renderHook(() => useUrlFiltering(initialParams), {
wrapper: Wrapper,
wrapperProps: { history },
});
@@ -62,7 +62,7 @@ test('extracting params from URL with advanced search and sort params', () => {
const history = createMemoryHistory({ initialEntries: [url] });
let result;
- result = renderHook(() => useUrlFiltering(initialSort), {
+ result = renderHook(() => useUrlFiltering(initialParams), {
wrapper: Wrapper,
wrapperProps: { history },
});
@@ -83,7 +83,7 @@ test('extracting params from URL with simple search param but no sort param', ()
const history = createMemoryHistory({ initialEntries: [url] });
let result;
- result = renderHook(() => useUrlFiltering(initialSort), {
+ result = renderHook(() => useUrlFiltering(initialParams), {
wrapper: Wrapper,
wrapperProps: { history },
});
@@ -103,7 +103,7 @@ test('extracting params from URL with no search param and with sort param with u
const history = createMemoryHistory({ initialEntries: [url] });
let result;
- result = renderHook(() => useUrlFiltering(initialSort), {
+ result = renderHook(() => useUrlFiltering(initialParams), {
wrapper: Wrapper,
wrapperProps: { history },
});
@@ -122,7 +122,7 @@ test('extracting params from URL with resource kinds', () => {
const history = createMemoryHistory({ initialEntries: [url] });
- const { current } = renderHook(() => useUrlFiltering(initialSort), {
+ const { current } = renderHook(() => useUrlFiltering(initialParams), {
wrapper: Wrapper,
wrapperProps: { history },
});
@@ -135,6 +135,8 @@ const initialSort: SortType = {
dir: 'ASC',
};
+const initialParams = { sort: initialSort };
+
function Wrapper(props: any) {
return {props.children};
}
diff --git a/web/packages/teleport/src/components/hooks/useUrlFiltering/useUrlFiltering.ts b/web/packages/teleport/src/components/hooks/useUrlFiltering/useUrlFiltering.ts
index e6312c9096246..f47ecc2234f3a 100644
--- a/web/packages/teleport/src/components/hooks/useUrlFiltering/useUrlFiltering.ts
+++ b/web/packages/teleport/src/components/hooks/useUrlFiltering/useUrlFiltering.ts
@@ -34,10 +34,12 @@ export interface UrlFilteringState {
search: string;
}
-export function useUrlFiltering(initialSort: SortType): UrlFilteringState {
+export function useUrlFiltering(
+ initialParams: Partial
+): UrlFilteringState {
const { search, pathname } = useLocation();
const [params, setParams] = useState({
- sort: initialSort,
+ ...initialParams,
...getResourceUrlQueryParams(search),
});
@@ -59,7 +61,8 @@ export function useUrlFiltering(initialSort: SortType): UrlFilteringState {
queryAfterLabelClick,
params.sort,
params.kinds,
- true /*isAdvancedSearch*/
+ true /*isAdvancedSearch*/,
+ params.pinnedOnly
)
);
};
@@ -84,6 +87,7 @@ export default function getResourceUrlQueryParams(
const searchParams = new URLSearchParams(searchPath);
const query = searchParams.get('query');
const search = searchParams.get('search');
+ const pinnedOnly = searchParams.get('pinnedOnly');
const sort = searchParams.get('sort');
const kinds = searchParams.has('kinds') ? searchParams.getAll('kinds') : null;
@@ -103,6 +107,8 @@ export default function getResourceUrlQueryParams(
kinds,
// Conditionally adds the sort field based on whether it exists or not
...(!!processedSortParam && { sort: processedSortParam }),
+ // Conditionally adds the pinnedResources field based on whether its true or not
+ ...(pinnedOnly === 'true' && { pinnedOnly: true }),
};
}
diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts
index 8ca868eb7dc26..ebf8d635cfe14 100644
--- a/web/packages/teleport/src/config.ts
+++ b/web/packages/teleport/src/config.ts
@@ -158,7 +158,7 @@ const cfg = {
passwordTokenPath: '/v1/webapi/users/password/token/:tokenId?',
changeUserPasswordPath: '/v1/webapi/users/password',
unifiedResourcesPath:
- '/v1/webapi/sites/:clusterId/resources?searchAsRoles=:searchAsRoles?&limit=:limit?&startKey=:startKey?&kinds=:kinds?&query=:query?&search=:search?&sort=:sort?',
+ '/v1/webapi/sites/:clusterId/resources?searchAsRoles=:searchAsRoles?&limit=:limit?&startKey=:startKey?&kinds=:kinds?&query=:query?&search=:search?&sort=:sort?&pinnedOnly=:pinnedOnly?',
nodesPath:
'/v1/webapi/sites/:clusterId/nodes?searchAsRoles=:searchAsRoles?&limit=:limit?&startKey=:startKey?&query=:query?&search=:search?&sort=:sort?',
nodesPathNoParams: '/v1/webapi/sites/:clusterId/nodes',
@@ -964,6 +964,7 @@ export interface UrlResourcesParams {
limit?: number;
startKey?: string;
searchAsRoles?: 'yes' | '';
+ pinnedOnly?: boolean;
// TODO(bl-nero): Remove this once filters are expressed as advanced search.
kinds?: string[];
}
diff --git a/web/packages/teleport/src/generateResourcePath.ts b/web/packages/teleport/src/generateResourcePath.ts
index 4a0212a990548..193af96b173e8 100644
--- a/web/packages/teleport/src/generateResourcePath.ts
+++ b/web/packages/teleport/src/generateResourcePath.ts
@@ -46,7 +46,8 @@ export default function generateResourcePath(
.replace(':search?', processedParams.search || '')
.replace(':searchAsRoles?', processedParams.searchAsRoles || '')
.replace(':sort?', processedParams.sort || '')
- .replace(':kinds?', processedParams.kinds || '');
+ .replace(':kinds?', processedParams.kinds || '')
+ .replace(':pinnedOnly?', processedParams.pinnedOnly || '');
return output;
}
diff --git a/web/packages/teleport/src/services/agents/types.ts b/web/packages/teleport/src/services/agents/types.ts
index ae03f775fb018..7f65b4dc8b6d2 100644
--- a/web/packages/teleport/src/services/agents/types.ts
+++ b/web/packages/teleport/src/services/agents/types.ts
@@ -54,6 +54,7 @@ export type ResourceFilter = {
sort?: SortType;
limit?: number;
startKey?: string;
+ pinnedOnly?: boolean;
// TODO(bl-nero): Remove this once filters are expressed as advanced search.
kinds?: string[];
};
diff --git a/web/packages/teleport/src/services/localStorage/localStorage.ts b/web/packages/teleport/src/services/localStorage/localStorage.ts
index 4e35b88607777..766544ff9ae74 100644
--- a/web/packages/teleport/src/services/localStorage/localStorage.ts
+++ b/web/packages/teleport/src/services/localStorage/localStorage.ts
@@ -199,6 +199,13 @@ const storage = {
return disabled !== 'true' && notSupported !== 'true';
},
+ arePinnedResourcesDisabled(): boolean {
+ return (
+ window.localStorage.getItem(KeysEnum.PINNED_RESOURCES_NOT_SUPPORTED) ===
+ 'true'
+ );
+ },
+
broadcast(messageType, messageBody) {
window.localStorage.setItem(messageType, messageBody);
window.localStorage.removeItem(messageType);
diff --git a/web/packages/teleport/src/services/localStorage/types.ts b/web/packages/teleport/src/services/localStorage/types.ts
index 1a6ef238a18bb..772fc5341c420 100644
--- a/web/packages/teleport/src/services/localStorage/types.ts
+++ b/web/packages/teleport/src/services/localStorage/types.ts
@@ -28,6 +28,7 @@ export const KeysEnum = {
UNIFIED_RESOURCES_DISABLED: 'grv_teleport_unified_resources_disabled',
UNIFIED_RESOURCES_NOT_SUPPORTED:
'grv_teleport_unified_resources_not_supported',
+ PINNED_RESOURCES_NOT_SUPPORTED: 'grv_teleport_pinned_resources_not_supported',
};
// SurveyRequest is the request for sending data to the back end
diff --git a/web/packages/teleport/src/services/userPreferences/index.ts b/web/packages/teleport/src/services/userPreferences/index.ts
index 904e36f89f252..0d43ccd45724f 100644
--- a/web/packages/teleport/src/services/userPreferences/index.ts
+++ b/web/packages/teleport/src/services/userPreferences/index.ts
@@ -14,4 +14,9 @@
* limitations under the License.
*/
-export { getUserPreferences, updateUserPreferences } from './userPreferences';
+export {
+ getUserPreferences,
+ updateUserPreferences,
+ updateUserClusterPreferences,
+ getUserClusterPreferences,
+} from './userPreferences';
diff --git a/web/packages/teleport/src/services/userPreferences/types.ts b/web/packages/teleport/src/services/userPreferences/types.ts
index 73b7ef59e4741..c0dc727c757c8 100644
--- a/web/packages/teleport/src/services/userPreferences/types.ts
+++ b/web/packages/teleport/src/services/userPreferences/types.ts
@@ -23,6 +23,11 @@ export enum ThemePreference {
Dark = 2,
}
+export enum UnifiedTabPreference {
+ All = 1,
+ Pinned = 2,
+}
+
export enum ClusterResource {
RESOURCE_UNSPECIFIED = 0,
RESOURCE_WINDOWS_DESKTOPS = 1,
@@ -49,6 +54,7 @@ export interface UserPreferences {
assist: AssistUserPreferences;
onboard: OnboardUserPreferences;
clusterPreferences: UserClusterPreferences;
+ unifiedResourcePreferences: UnifiedResourcePreferences;
}
// UserClusterPreferences are user preferences that are
@@ -58,6 +64,12 @@ export interface UserClusterPreferences {
pinnedResources: string[];
}
+// UnifiedResourcePreferences are preferences related to the Unified Resource view
+export interface UnifiedResourcePreferences {
+ // defaultTab is the default tab selected in the unified resource view
+ defaultTab: UnifiedTabPreference;
+}
+
export type GetUserClusterPreferencesResponse = UserClusterPreferences;
export type GetUserPreferencesResponse = UserPreferences;
diff --git a/web/packages/teleport/src/services/userPreferences/userPreferences.ts b/web/packages/teleport/src/services/userPreferences/userPreferences.ts
index eb7bd04c5103a..4860ab1e1dc35 100644
--- a/web/packages/teleport/src/services/userPreferences/userPreferences.ts
+++ b/web/packages/teleport/src/services/userPreferences/userPreferences.ts
@@ -18,10 +18,14 @@ import cfg from 'teleport/config';
import api from 'teleport/services/api';
import { ViewMode } from 'teleport/Assist/types';
-import { ThemePreference } from 'teleport/services/userPreferences/types';
+import {
+ ThemePreference,
+ UnifiedTabPreference,
+} from 'teleport/services/userPreferences/types';
+
+import { KeysEnum } from '../localStorage';
import type {
- GetUserClusterPreferencesResponse,
GetUserPreferencesResponse,
UserClusterPreferences,
UserPreferences,
@@ -36,16 +40,32 @@ export async function getUserPreferences() {
}
export async function getUserClusterPreferences(clusterId: string) {
- const res: GetUserClusterPreferencesResponse = await api.get(
- cfg.getUserClusterPreferencesUrl(clusterId)
- );
-
- return res;
+ return await api
+ .get(cfg.getUserClusterPreferencesUrl(clusterId))
+ .then(res => {
+ // TODO (avatus) DELETE IN 15
+ // this item is used to disabled the pinned resources button if they
+ // haven't upgraded to 14.1.0 yet. Anything lower than 14 doesn't matter
+ // because the unified resource view isn't enabled so pinning isn't there either
+ localStorage.removeItem(KeysEnum.PINNED_RESOURCES_NOT_SUPPORTED);
+ return res;
+ })
+ .catch(res => {
+ if (res.response?.status === 403 || res.response?.status === 404) {
+ localStorage.setItem(KeysEnum.PINNED_RESOURCES_NOT_SUPPORTED, 'true');
+ // we handle this null error in the user context where we cache cluster
+ // preferences. We want to fail gracefully here and use our "not supported"
+ // message instead.
+ return null;
+ }
+ // return all other errors here
+ return res;
+ });
}
export function updateUserClusterPreferences(
clusterId: string,
- preferences: Partial
+ preferences: UserPreferences
) {
return api.put(cfg.getUserClusterPreferencesUrl(clusterId), preferences);
}
@@ -70,6 +90,9 @@ export function makeDefaultUserPreferences(): UserPreferences {
intent: '',
},
},
+ unifiedResourcePreferences: {
+ defaultTab: UnifiedTabPreference.All,
+ },
clusterPreferences: makeDefaultUserClusterPreferences(),
};
}