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 && } + + `margin-top: 36px;`} transformOrigin={{ @@ -224,7 +239,7 @@ const FilterTypesMenu = ({ key={kind.value} onClick={() => handleSelect(kind.value)} > - { @@ -233,7 +248,7 @@ const FilterTypesMenu = ({ id={kind.value} checked={kinds.includes(kind.value)} /> - + {kind.label} @@ -289,21 +304,23 @@ const SortMenu: React.FC = props => { }; return ( - - props.theme.colors.spotBackground[0]}; - `} - textTransform="none" - size="small" - px={2} - onClick={handleOpen} - > - {sortType} - + + Sort by}> + props.theme.colors.spotBackground[0]}; + `} + textTransform="none" + size="small" + px={2} + onClick={handleOpen} + > + {sortType} + + `margin-top: 36px;`} transformOrigin={{ @@ -321,19 +338,21 @@ const SortMenu: React.FC = props => { handleSelect('name')}>Name handleSelect('kind')}>Type - props.theme.colors.spotBackground[0]}; - `} - size="small" - > - {sortDir === 'ASC' ? : } - + Sort direction}> + props.theme.colors.spotBackground[0]}; + `} + size="small" + > + {sortDir === 'ASC' ? : } + + ); }; diff --git a/web/packages/teleport/src/UnifiedResources/ResourceCard.story.tsx b/web/packages/teleport/src/UnifiedResources/ResourceCard.story.tsx index ebe5fba40b400..358852231ff92 100644 --- a/web/packages/teleport/src/UnifiedResources/ResourceCard.story.tsx +++ b/web/packages/teleport/src/UnifiedResources/ResourceCard.story.tsx @@ -101,7 +101,16 @@ export const Cards: Story = { ...additionalResources, ...desktops, ].map((res, i) => ( - + {}} + pinningDisabled={false} + selectResource={() => {}} + selected={false} + pinningNotSupported={false} + /> ))} diff --git a/web/packages/teleport/src/UnifiedResources/ResourceCard.tsx b/web/packages/teleport/src/UnifiedResources/ResourceCard.tsx index 4ecffbd3f9713..fca662233ca7d 100644 --- a/web/packages/teleport/src/UnifiedResources/ResourceCard.tsx +++ b/web/packages/teleport/src/UnifiedResources/ResourceCard.tsx @@ -23,16 +23,9 @@ import React, { } from 'react'; import styled, { css } from 'styled-components'; -import { - Box, - ButtonIcon, - ButtonLink, - Flex, - Label, - Popover, - Text, -} from 'design'; +import { Box, ButtonIcon, ButtonLink, Flex, Label, Text } from 'design'; import copyToClipboard from 'design/utils/copyToClipboard'; +import { StyledCheckbox } from 'design/Checkbox'; import { ShimmerBox } from 'design/ShimmerBox'; import { ResourceIcon, ResourceIconName } from 'design/ResourceIcon'; @@ -44,6 +37,8 @@ import { Kubernetes as KubernetesIcon, Server as ServersIcon, Desktop as DesktopsIcon, + PushPinFilled, + PushPin, } from 'design/Icon'; import { @@ -54,7 +49,12 @@ import { import { Database } from 'teleport/services/databases'; import { ResourceActionButton } from './ResourceActionButton'; -import { resourceName } from './Resources'; +import { + resourceKey, + resourceName, + HoverTooltip, + PINNING_NOT_SUPPORTED_MESSAGE, +} from './Resources'; // Since we do a lot of manual resizing and some absolute positioning, we have // to put some layout constants in place here. @@ -75,10 +75,30 @@ const ResTypeIconBox = styled(Box)` type Props = { resource: UnifiedResource; onLabelClick?: (label: ResourceLabel) => void; + pinResource: (id: string) => void; + selectResource: (id: string) => void; + pinned: boolean; + selected: boolean; + // this is used to disable pinning functionality if + // a leaf cluster hasn't been upgraded yet + pinningNotSupported: boolean; + // pinningDisabled is used to disable the button during + // a pinning network request + pinningDisabled: boolean; }; -export function ResourceCard({ resource, onLabelClick }: Props) { +export function ResourceCard({ + resource, + onLabelClick, + pinned = false, + selected = false, + pinResource, + selectResource, + pinningNotSupported = false, + pinningDisabled, +}: Props) { const name = resourceName(resource); + const id = resourceKey(resource); const resIcon = resourceIconName(resource); const ResTypeIcon = resourceTypeIcon(resource.kind); const description = resourceDescription(resource); @@ -174,89 +194,118 @@ export function ResourceCard({ resource, onLabelClick }: Props) { } }; + const setPinned = () => { + pinResource(id); + }; + return ( setHovered(true)} onMouseLeave={() => setHovered(false)} > - - - {/* MinWidth is important to prevent descriptions from overflowing. */} - - - - {isNameOverflowed ? ( - {name}}> + + + {selected ? 'Deselect' : 'Select'}}> + selectResource(id)} + /> + + + + {/* MinWidth is important to prevent descriptions from overflowing. */} + + + + {isNameOverflowed ? ( + {name}}> + + {name} + + + ) : ( {name} - - ) : ( - - {name} - - )} - - {hovered && } - - - - - - - {description.primary && ( - - - {description.primary} - + )} - )} - {description.secondary && ( - - - {description.secondary} - - - )} + {hovered && } + + + + + + + {description.primary && ( + + + {description.primary} + + + )} + {description.secondary && ( + + + {description.secondary} + + + )} + + + + + + {numMoreLabels} more + + {resource.labels.map((label, i) => { + const { name, value } = label; + const labelText = `${name}: ${value}`; + return ( + onLabelClick?.(label)} + kind="secondary" + data-is-label="" + > + {labelText} + + ); + })} + + - - - - + {numMoreLabels} more - - {resource.labels.map((label, i) => { - const { name, value } = label; - const labelText = `${name}: ${value}`; - return ( - onLabelClick?.(label)} - kind="secondary" - data-is-label="" - > - {labelText} - - ); - })} - - - - + + ); } @@ -443,25 +492,7 @@ const CardContainer = styled(Box)` position: relative; `; -const elevatedCardMixin = css` - background-color: ${props => props.theme.colors.levels.elevated}; - border-color: ${props => props.theme.colors.levels.elevated}; - box-shadow: ${props => props.theme.boxShadow[1]}; -`; - -/** - * The inner container that normally holds a regular layout of the card, and is - * fully contained inside the outer container. Once the user clicks the "more" - * button, the inner container "pops out" by changing its position to absolute. - * - * TODO(bl-nero): Known issue: this doesn't really work well with one-column - * layout; we may need to globally set the card height to fixed size on the - * outer container. - */ -const CardInnerContainer = styled(Flex)` - background-color: transparent; - border: ${props => props.theme.borders[2]} - ${props => props.theme.colors.spotBackground[0]}; +const CardOuterContainer = styled(Box)` border-radius: ${props => props.theme.radii[3]}px; ${props => @@ -471,76 +502,44 @@ const CardInnerContainer = styled(Flex)` left: 0; right: 0; z-index: 1; - ${elevatedCardMixin} `} - transition: all 150ms; ${CardContainer}:hover & { - ${elevatedCardMixin} + background-color: ${props => props.theme.colors.levels.surface}; } `; -const SingleLineBox = styled(Box)` - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; +/** + * The inner container that normally holds a regular layout of the card, and is + * fully contained inside the outer container. Once the user clicks the "more" + * button, the inner container "pops out" by changing its position to absolute. + * + * TODO(bl-nero): Known issue: this doesn't really work well with one-column + * layout; we may need to globally set the card height to fixed size on the + * outer container. + */ +const CardInnerContainer = styled(Flex)` + border: ${props => props.theme.borders[2]} + ${props => props.theme.colors.spotBackground[0]}; + border-radius: ${props => props.theme.radii[3]}px; + background-color: ${props => getBackgroundColor(props)}; `; -export const HoverTooltip: React.FC<{ - tipContent: React.ReactElement; - fontSize?: number; -}> = ({ tipContent, fontSize = 10, children }) => { - const [anchorEl, setAnchorEl] = useState(); - const open = Boolean(anchorEl); - - function handlePopoverOpen(event) { - setAnchorEl(event.currentTarget); +const getBackgroundColor = props => { + if (props.selected) { + return props.theme.colors.interactive.tonal.primary[2]; } - - function handlePopoverClose() { - setAnchorEl(null); + if (props.pinned) { + return props.theme.colors.interactive.tonal.primary[0]; } - - return ( - <> - - {children} - - - - {tipContent} - - - - ); + return 'transparent'; }; -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; +const SingleLineBox = styled(Box)` + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; `; /** @@ -571,6 +570,7 @@ const LabelsInnerContainer = styled(Flex)` flex-wrap: wrap; align-items: start; gap: ${props => props.theme.space[1]}px; + padding-right: 60px; `; /** @@ -586,17 +586,12 @@ const MoreLabelsButton = styled(ButtonLink)` margin: ${labelVerticalMargin}px 0; min-height: 0; - background-color: ${props => props.theme.colors.levels.sunken}; + background-color: ${props => getBackgroundColor(props)}; color: ${props => props.theme.colors.text.slightlyMuted}; font-style: italic; - border-radius: 0; transition: visibility 0s; transition: background 150ms; - - ${CardContainer}:hover & { - background-color: ${props => props.theme.colors.levels.elevated}; - } `; const LoadingCardWrapper = styled(Box)` @@ -605,3 +600,49 @@ const LoadingCardWrapper = styled(Box)` ${props => props.theme.colors.spotBackground[0]}; border-radius: ${props => props.theme.radii[3]}px; `; + +function PinButton({ + pinned, + hovered, + setPinned, + pinningDisabled, + pinningNotSupported, +}: { + pinned: boolean; + hovered: boolean; + setPinned: (id: string) => void; + pinningDisabled: boolean; + pinningNotSupported: boolean; +}) { + const copyAnchorEl = useRef(null); + const tipContent = pinningNotSupported + ? PINNING_NOT_SUPPORTED_MESSAGE + : pinned + ? 'Unpin' + : 'Pin'; + + return ( + props.theme.space[9]}px; + left: 16px; + `} + disabled={pinningDisabled || pinningNotSupported} + setRef={copyAnchorEl} + size={0} + onClick={setPinned} + > + {tipContent}}> + {pinned ? ( + + ) : ( + + )} + + + ); +} diff --git a/web/packages/teleport/src/UnifiedResources/ResourceTab.tsx b/web/packages/teleport/src/UnifiedResources/ResourceTab.tsx new file mode 100644 index 0000000000000..3541e3526b45b --- /dev/null +++ b/web/packages/teleport/src/UnifiedResources/ResourceTab.tsx @@ -0,0 +1,78 @@ +/** + * 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. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { Box, Text } from 'design'; + +import { HoverTooltip, PINNING_NOT_SUPPORTED_MESSAGE } from './Resources'; + +export const ResourceTab = ({ + title, + disabled, + isSelected, + onClick, +}: Props) => { + const handleClick = () => { + if (!disabled) { + onClick(); + } + }; + + const $tab = ( + + {title} + + ); + + if (disabled) { + return ( + {PINNING_NOT_SUPPORTED_MESSAGE}}> + {$tab} + + ); + } + return $tab; +}; + +const TabBox = styled(Box)` + cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')}; + color: ${props => + props.disabled + ? props.theme.colors.text.disabled + : props.theme.colors.text.main}; + border-bottom: ${props => + props.selected ? `2px solid ${props.theme.colors.brand}` : 'transparent'}; +`; + +const TabText = styled(Text)` + font-size: ${props => props.theme.fontSizes[2]}; + font-weight: ${props => + props.selected + ? props.theme.fontWeights.bold + : props.theme.fontWeights.regular}; + line-height: 20px; + + color: ${props => + props.selected ? props.theme.colors.brand : props.theme.colors.main}; +`; + +type Props = { + title: string; + isSelected: boolean; + disabled: boolean; + onClick: () => void; +}; diff --git a/web/packages/teleport/src/UnifiedResources/Resources.tsx b/web/packages/teleport/src/UnifiedResources/Resources.tsx index f81a04c63badc..407478b7e75ff 100644 --- a/web/packages/teleport/src/UnifiedResources/Resources.tsx +++ b/web/packages/teleport/src/UnifiedResources/Resources.tsx @@ -14,13 +14,26 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; -import { Box, Flex, ButtonLink, ButtonSecondary, Text } from 'design'; -import { Magnifier } from 'design/Icon'; +import { + Box, + Flex, + ButtonLink, + ButtonSecondary, + Text, + ButtonBorder, + Popover, +} from 'design'; +import { Magnifier, PushPin } from 'design/Icon'; import { Danger } from 'design/Alert'; +import { + makeEmptyAttempt, + makeSuccessAttempt, + useAsync, +} from 'shared/hooks/useAsync'; import { TextIcon } from 'teleport/Discover/Shared'; @@ -39,7 +52,11 @@ import AgentButtonAdd from 'teleport/components/AgentButtonAdd'; import { SearchResource } from 'teleport/Discover/SelectResource'; import { useUrlFiltering, useInfiniteScroll } from 'teleport/components/hooks'; import { UnifiedResource } from 'teleport/services/agents'; +import { useUser } from 'teleport/User/UserContext'; +import { encodeUrlQueryParams } from 'teleport/components/hooks/useUrlFiltering'; +import { UnifiedTabPreference } from 'teleport/services/userPreferences/types'; +import { ResourceTab } from './ResourceTab'; import { ResourceCard, LoadingCard } from './ResourceCard'; import SearchPanel from './SearchPanel'; import { FilterPanel } from './FilterPanel'; @@ -53,19 +70,117 @@ const FETCH_MORE_SIZE = 24; const loadingCardArray = new Array(FETCH_MORE_SIZE).fill(undefined); +export const PINNING_NOT_SUPPORTED_MESSAGE = + 'This cluster does not support pinning resources. To enable, upgrade to 14.1 or newer.'; + +const tabs: { label: string; value: UnifiedTabPreference }[] = [ + { + label: 'All Resources', + value: UnifiedTabPreference.All, + }, + { + label: 'Pinned Resources', + value: UnifiedTabPreference.Pinned, + }, +]; + export function Resources() { - const { isLeafCluster } = useStickyClusterId(); + const { isLeafCluster, clusterId } = useStickyClusterId(); const enabled = localStorage.areUnifiedResourcesEnabled(); + const pinningNotSupported = localStorage.arePinnedResourcesDisabled(); + const { + getClusterPinnedResources, + preferences, + updatePreferences, + updateClusterPinnedResources, + } = useUser(); const teleCtx = useTeleport(); const canCreate = teleCtx.storeUser.getTokenAccess().create; - const { clusterId } = useStickyClusterId(); + const [selectedResources, setSelectedResources] = useState([]); + + const [getPinnedResourcesAttempt, getPinnedResources, setPinnedResources] = + useAsync( + useCallback( + () => getClusterPinnedResources(clusterId), + [clusterId, getClusterPinnedResources] + ) + ); + + useEffect(() => { + getPinnedResources(); + setSelectedResources([]); + setUpdatePinnedResources(makeEmptyAttempt()); + }, [clusterId, getPinnedResources]); + + const pinnedResources = getPinnedResourcesAttempt.data || []; + + const [ + updatePinnedResourcesAttempt, + updatePinnedResources, + setUpdatePinnedResources, + ] = useAsync(async (newPinnedResources: string[]) => { + await updateClusterPinnedResources(clusterId, newPinnedResources); + setPinnedResources(makeSuccessAttempt(newPinnedResources)); + }); const { params, setParams, replaceHistory, pathname, setSort, onLabelClick } = useUrlFiltering({ - fieldName: 'name', - dir: 'ASC', + sort: { + fieldName: 'name', + dir: 'ASC', + }, + pinnedOnly: + preferences.unifiedResourcePreferences.defaultTab === + UnifiedTabPreference.Pinned, }); + useEffect(() => { + const handleKeyDown = event => { + if (event.key === 'Escape') { + setSelectedResources([]); + } + }; + document.addEventListener('keydown', handleKeyDown); + + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, []); + + const handlePinResource = (resourceId: string) => { + if (pinnedResources.includes(resourceId)) { + updatePinnedResources(pinnedResources.filter(i => i !== resourceId)); + return; + } + updatePinnedResources([...pinnedResources, resourceId]); + }; + + // if every selected resource is already pinned, the bulk action + // should be to unpin those resources + const shouldUnpin = selectedResources.every(resource => + pinnedResources.includes(resource) + ); + + const handleSelectResources = (resourceId: string) => { + if (selectedResources.includes(resourceId)) { + setSelectedResources(selectedResources.filter(i => i !== resourceId)); + return; + } + setSelectedResources([...selectedResources, resourceId]); + }; + + const handlePinSelected = (unpin: boolean) => { + let newPinned = []; + if (unpin) { + newPinned = pinnedResources.filter(i => !selectedResources.includes(i)); + } else { + const combined = [...pinnedResources, ...selectedResources]; + newPinned = Array.from(new Set(combined)); + } + + updatePinnedResources(newPinned); + }; + const { setTrigger: setScrollDetector, forceFetch, @@ -97,6 +212,58 @@ export function Resources() { forceFetch(); }; + const allSelected = + resources.length > 0 && + resources.every(resource => + selectedResources.includes(resourceKey(resource)) + ); + + const toggleSelectVisible = () => { + if (allSelected) { + setSelectedResources([]); + return; + } + setSelectedResources(resources.map(resource => resourceKey(resource))); + }; + + const selectTab = (value: UnifiedTabPreference) => { + const pinnedOnly = value === UnifiedTabPreference.Pinned; + setParams({ + ...params, + pinnedOnly, + }); + setSelectedResources([]); + setUpdatePinnedResources(makeEmptyAttempt()); + setPinnedResources(makeSuccessAttempt(getPinnedResourcesAttempt.data)); + updatePreferences({ unifiedResourcePreferences: { defaultTab: value } }); + replaceHistory( + encodeUrlQueryParams( + pathname, + params.search, + params.sort, + params.kinds, + !!params.query /* isAdvancedSearch */, + pinnedOnly + ) + ); + }; + + const $pinAllButton = ( + handlePinSelected(shouldUnpin)} + textTransform="none" + disabled={pinningNotSupported} + css={` + border: none; + color: ${props => props.theme.colors.brand}; + `} + > + + {shouldUnpin ? 'Unpin ' : 'Pin '} + Selected + + ); + return ( )} + {getPinnedResourcesAttempt.status === 'error' && ( + + {getPinnedResourcesAttempt.statusText} + + )} + {updatePinnedResourcesAttempt.status === 'error' && ( + + {updatePinnedResourcesAttempt.statusText} + + )} - + + + {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(), }; }