feat(frontend): add stripe link to settings sidebar

Adds a link to stripe in the omni settings sidebar if stripe is enabled in omni

Signed-off-by: Edward Sammut Alessi <edward.sammutalessi@siderolabs.com>
This commit is contained in:
Edward Sammut Alessi 2025-10-06 19:19:39 +02:00
parent ef84a4cafa
commit af3eeaf47f
No known key found for this signature in database
GPG Key ID: 65558E016966977A
24 changed files with 702 additions and 411 deletions

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.7
// protoc-gen-go v1.36.9
// protoc v5.29.4
// source: common/omni.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.7
// protoc-gen-go v1.36.9
// protoc v5.29.4
// source: omni/management/management.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.7
// protoc-gen-go v1.36.9
// protoc v5.29.4
// source: omni/oidc/oidc.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.7
// protoc-gen-go v1.36.9
// protoc v5.29.4
// source: omni/resources/resources.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.7
// protoc-gen-go v1.36.9
// protoc v5.29.4
// source: omni/specs/auth.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.7
// protoc-gen-go v1.36.9
// protoc v5.29.4
// source: omni/specs/ephemeral.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.7
// protoc-gen-go v1.36.9
// protoc v5.29.4
// source: omni/specs/infra.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.7
// protoc-gen-go v1.36.9
// protoc v5.29.4
// source: omni/specs/oidc.proto

File diff suppressed because it is too large Load Diff

View File

@ -1029,12 +1029,19 @@ message FeaturesConfigSpec {
// UserPilotSettings enables UserPilot on the frontend side.
UserPilotSettings user_pilot_settings = 6;
// StripeSettings enables Stripe on the frontend side.
StripeSettings stripe_settings = 7;
}
message UserPilotSettings {
string app_token = 1;
}
message StripeSettings {
bool enabled = 1;
}
message EtcdBackupSettings {
// TickInterval is the interval between checking for backups in controller.
google.protobuf.Duration tick_interval = 1;

View File

@ -1710,6 +1710,7 @@ func (m *FeaturesConfigSpec) CloneVT() *FeaturesConfigSpec {
r.AuditLogEnabled = m.AuditLogEnabled
r.ImageFactoryBaseUrl = m.ImageFactoryBaseUrl
r.UserPilotSettings = m.UserPilotSettings.CloneVT()
r.StripeSettings = m.StripeSettings.CloneVT()
if len(m.unknownFields) > 0 {
r.unknownFields = make([]byte, len(m.unknownFields))
copy(r.unknownFields, m.unknownFields)
@ -1738,6 +1739,23 @@ func (m *UserPilotSettings) CloneMessageVT() proto.Message {
return m.CloneVT()
}
func (m *StripeSettings) CloneVT() *StripeSettings {
if m == nil {
return (*StripeSettings)(nil)
}
r := new(StripeSettings)
r.Enabled = m.Enabled
if len(m.unknownFields) > 0 {
r.unknownFields = make([]byte, len(m.unknownFields))
copy(r.unknownFields, m.unknownFields)
}
return r
}
func (m *StripeSettings) CloneMessageVT() proto.Message {
return m.CloneVT()
}
func (m *EtcdBackupSettings) CloneVT() *EtcdBackupSettings {
if m == nil {
return (*EtcdBackupSettings)(nil)
@ -5001,6 +5019,9 @@ func (this *FeaturesConfigSpec) EqualVT(that *FeaturesConfigSpec) bool {
if !this.UserPilotSettings.EqualVT(that.UserPilotSettings) {
return false
}
if !this.StripeSettings.EqualVT(that.StripeSettings) {
return false
}
return string(this.unknownFields) == string(that.unknownFields)
}
@ -5030,6 +5051,25 @@ func (this *UserPilotSettings) EqualMessageVT(thatMsg proto.Message) bool {
}
return this.EqualVT(that)
}
func (this *StripeSettings) EqualVT(that *StripeSettings) bool {
if this == that {
return true
} else if this == nil || that == nil {
return false
}
if this.Enabled != that.Enabled {
return false
}
return string(this.unknownFields) == string(that.unknownFields)
}
func (this *StripeSettings) EqualMessageVT(thatMsg proto.Message) bool {
that, ok := thatMsg.(*StripeSettings)
if !ok {
return false
}
return this.EqualVT(that)
}
func (this *EtcdBackupSettings) EqualVT(that *EtcdBackupSettings) bool {
if this == that {
return true
@ -10908,6 +10948,16 @@ func (m *FeaturesConfigSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
i -= len(m.unknownFields)
copy(dAtA[i:], m.unknownFields)
}
if m.StripeSettings != nil {
size, err := m.StripeSettings.MarshalToSizedBufferVT(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = protohelpers.EncodeVarint(dAtA, i, uint64(size))
i--
dAtA[i] = 0x3a
}
if m.UserPilotSettings != nil {
size, err := m.UserPilotSettings.MarshalToSizedBufferVT(dAtA[:i])
if err != nil {
@ -11008,6 +11058,49 @@ func (m *UserPilotSettings) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
return len(dAtA) - i, nil
}
func (m *StripeSettings) MarshalVT() (dAtA []byte, err error) {
if m == nil {
return nil, nil
}
size := m.SizeVT()
dAtA = make([]byte, size)
n, err := m.MarshalToSizedBufferVT(dAtA[:size])
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *StripeSettings) MarshalToVT(dAtA []byte) (int, error) {
size := m.SizeVT()
return m.MarshalToSizedBufferVT(dAtA[:size])
}
func (m *StripeSettings) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
if m == nil {
return 0, nil
}
i := len(dAtA)
_ = i
var l int
_ = l
if m.unknownFields != nil {
i -= len(m.unknownFields)
copy(dAtA[i:], m.unknownFields)
}
if m.Enabled {
i--
if m.Enabled {
dAtA[i] = 1
} else {
dAtA[i] = 0
}
i--
dAtA[i] = 0x8
}
return len(dAtA) - i, nil
}
func (m *EtcdBackupSettings) MarshalVT() (dAtA []byte, err error) {
if m == nil {
return nil, nil
@ -15180,6 +15273,10 @@ func (m *FeaturesConfigSpec) SizeVT() (n int) {
l = m.UserPilotSettings.SizeVT()
n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
}
if m.StripeSettings != nil {
l = m.StripeSettings.SizeVT()
n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
}
n += len(m.unknownFields)
return n
}
@ -15198,6 +15295,19 @@ func (m *UserPilotSettings) SizeVT() (n int) {
return n
}
func (m *StripeSettings) SizeVT() (n int) {
if m == nil {
return 0
}
var l int
_ = l
if m.Enabled {
n += 2
}
n += len(m.unknownFields)
return n
}
func (m *EtcdBackupSettings) SizeVT() (n int) {
if m == nil {
return 0
@ -27975,6 +28085,42 @@ func (m *FeaturesConfigSpec) UnmarshalVT(dAtA []byte) error {
return err
}
iNdEx = postIndex
case 7:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field StripeSettings", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return protohelpers.ErrIntOverflow
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return protohelpers.ErrInvalidLength
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return protohelpers.ErrInvalidLength
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.StripeSettings == nil {
m.StripeSettings = &StripeSettings{}
}
if err := m.StripeSettings.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := protohelpers.Skip(dAtA[iNdEx:])
@ -28080,6 +28226,77 @@ func (m *UserPilotSettings) UnmarshalVT(dAtA []byte) error {
}
return nil
}
func (m *StripeSettings) UnmarshalVT(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return protohelpers.ErrIntOverflow
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: StripeSettings: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: StripeSettings: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Enabled", wireType)
}
var v int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return protohelpers.ErrIntOverflow
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
v |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
m.Enabled = bool(v != 0)
default:
iNdEx = preIndex
skippy, err := protohelpers.Skip(dAtA[iNdEx:])
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
return protohelpers.ErrInvalidLength
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...)
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *EtcdBackupSettings) UnmarshalVT(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.7
// protoc-gen-go v1.36.9
// protoc v5.29.4
// source: omni/specs/siderolink.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.7
// protoc-gen-go v1.36.9
// protoc v5.29.4
// source: omni/specs/system.proto

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.7
// protoc-gen-go v1.36.9
// protoc v5.29.4
// source: omni/specs/virtual.proto

View File

@ -673,12 +673,17 @@ export type FeaturesConfigSpec = {
audit_log_enabled?: boolean
image_factory_base_url?: string
user_pilot_settings?: UserPilotSettings
stripe_settings?: StripeSettings
}
export type UserPilotSettings = {
app_token?: string
}
export type StripeSettings = {
enabled?: boolean
}
export type EtcdBackupSettings = {
tick_interval?: GoogleProtobufDuration.Duration
min_interval?: GoogleProtobufDuration.Duration

View File

@ -20,6 +20,7 @@ export type SideBarItem = {
tooltip?: string
iconSvgBase64?: string
subItems?: SideBarItem[]
regularLink?: boolean
}
type Props = {
@ -36,6 +37,7 @@ defineProps<Props>()
v-for="item of items"
:key="item.name"
:route="item.route"
:regular-link="item.regularLink"
:name="item.name"
:icon="item.icon"
:icon-svg-base64="item.iconSvgBase64"

View File

@ -35,14 +35,8 @@ const expanded = useSessionStorage(`sidebar-expanded-${props.level}-${props.name
const vueroute = useRoute()
const { subItems, level } = toRefs(props)
const handleClick = (event: Event) => {
if (props.regularLink) {
return // Don't prevent default for regular links
}
event.preventDefault()
if (!props.route) {
const toggleSubmenu = () => {
if (subItems.value?.length) {
expanded.value = !expanded.value
}
}
@ -67,7 +61,7 @@ const componentType = props.route ? (props.regularLink ? 'a' : 'router-link') :
const componentAttributes = props.route
? props.regularLink
? { href: props.route, target: '_blank' }
? { href: props.route, target: '_blank', rel: 'noopener noreferrer' }
: { to: props.route, exactActiveClass: 'item-active' }
: { class: 'select-none cursor-pointer' }
@ -75,13 +69,14 @@ componentAttributes.class = (componentAttributes.class ?? '') + ' item-container
</script>
<template>
<component :is="componentType" v-bind="componentAttributes" @click="handleClick">
<component :is="componentType" v-bind="componentAttributes">
<Tooltip placement="right" :description="tooltip" :offset-distance="10" :offset-skid="0">
<div class="flex w-full flex-col">
<div
class="item w-full"
:class="{ 'sub-item': level > 0, root: level === 0 }"
:style="{ 'padding-left': `${24 * (level + 1)}px` }"
@click="toggleSubmenu"
>
<TIcon
v-if="icon || iconSvgBase64"
@ -99,7 +94,6 @@ componentAttributes.class = (componentAttributes.class ?? '') + ' item-container
class="transition-color h-6 w-6 transition-transform duration-250 hover:text-naturals-n13"
:class="{ 'rotate-180': !expanded }"
icon="drop-up"
@click.stop.prevent="() => (expanded = !expanded)"
/>
</div>
</div>
@ -107,7 +101,6 @@ componentAttributes.class = (componentAttributes.class ?? '') + ' item-container
v-if="expanded"
class="relative overflow-hidden"
:class="{ 'submenu-bg': level === 0 }"
@click.stop.prevent
>
<div
v-for="(item, index) in subItems ?? []"

View File

@ -20,6 +20,8 @@ vi.mock('@/api/grpc', () => ({
vi.mock('@/api/resources', () => ({
DefaultNamespace: 'default',
ClusterType: 'Cluster',
FeaturesConfigType: 'FeaturesConfig',
FeaturesConfigID: 'FeaturesConfigID',
}))
vi.mock('@/api/options', () => ({

View File

@ -5,8 +5,6 @@
import { useLocalStorage } from '@vueuse/core'
import { Userpilot } from 'userpilot'
import type { Ref } from 'vue'
import { computed, ref } from 'vue'
import { Runtime } from '@/api/common/omni.pb'
import type { Resource } from '@/api/grpc'
@ -15,23 +13,19 @@ import type { FeaturesConfigSpec } from '@/api/omni/specs/omni.pb'
import type { CurrentUserSpec } from '@/api/omni/specs/virtual.pb'
import { withAbortController, withRuntime } from '@/api/options'
import { DefaultNamespace, FeaturesConfigID, FeaturesConfigType } from '@/api/resources'
import Watch from '@/api/watch'
import { useWatch } from '@/components/common/Watch/useWatch'
export const setupWorkloadProxyingEnabledFeatureWatch = (): Ref<boolean> => {
const featuresConfig: Ref<Resource<FeaturesConfigSpec> | undefined> = ref()
const featuresConfigWatch = new Watch(featuresConfig)
featuresConfigWatch.setup({
resource: {
type: FeaturesConfigType,
namespace: DefaultNamespace,
id: FeaturesConfigID,
},
const resource = {
type: FeaturesConfigType,
namespace: DefaultNamespace,
id: FeaturesConfigID,
}
export function useFeatures() {
return useWatch<FeaturesConfigSpec>({
resource,
runtime: Runtime.Omni,
})
return computed(() => {
return featuresConfig?.value?.spec?.enable_workload_proxying ?? false
})
}
let userPilotInitialized = false
@ -106,14 +100,7 @@ export const getImageFactoryBaseURL = async (): Promise<string> => {
const getFeaturesConfig = async (): Promise<Resource<FeaturesConfigSpec>> => {
if (!cachedFeaturesConfig) {
cachedFeaturesConfig = await ResourceService.Get(
{
type: FeaturesConfigType,
namespace: DefaultNamespace,
id: FeaturesConfigID,
},
withRuntime(Runtime.Omni),
)
cachedFeaturesConfig = await ResourceService.Get(resource, withRuntime(Runtime.Omni))
}
return cachedFeaturesConfig

View File

@ -47,10 +47,7 @@ import {
setClusterWorkloadProxy,
setUseEmbeddedDiscoveryService,
} from '@/methods/cluster'
import {
embeddedDiscoveryServiceFeatureAvailable,
setupWorkloadProxyingEnabledFeatureWatch,
} from '@/methods/features'
import { embeddedDiscoveryServiceFeatureAvailable, useFeatures } from '@/methods/features'
import ClusterMachines from '@/views/cluster/ClusterMachines/ClusterMachines.vue'
import OverviewRightPanel from '@/views/cluster/Overview/components/OverviewRightPanel/OverviewRightPanel.vue'
import ClusterEtcdBackupCheckbox from '@/views/omni/Clusters/ClusterEtcdBackupCheckbox.vue'
@ -153,7 +150,7 @@ const { canManageClusterFeatures } = setupClusterPermissions(
computed(() => currentCluster.value.metadata.id as string),
)
const workloadProxyingEnabled = setupWorkloadProxyingEnabledFeatureWatch()
const { data: features } = useFeatures()
const isEmbeddedDiscoveryServiceAvailable = ref(false)
@ -350,7 +347,10 @@ onMounted(async () => {
</div>
</div>
<div class="flex gap-5">
<div v-if="workloadProxyingEnabled" class="overview-card mb-5 flex-1 px-6">
<div
v-if="features?.spec.enable_workload_proxying"
class="overview-card mb-5 flex-1 px-6"
>
<div class="mb-3">
<span class="overview-box-title">Features</span>
</div>

View File

@ -17,7 +17,7 @@ import Watch from '@/api/watch'
import type { SideBarItem } from '@/components/SideBar/TSideBarList.vue'
import { default as TSidebarList } from '@/components/SideBar/TSideBarList.vue'
import { setupClusterPermissions } from '@/methods/auth'
import { setupWorkloadProxyingEnabledFeatureWatch } from '@/methods/features'
import { useFeatures } from '@/methods/features'
import ExposedServiceSideBar from '@/views/cluster/ExposedService/ExposedServiceSideBar.vue'
const route = useRoute()
@ -106,7 +106,7 @@ const items = computed(() => {
return result
})
const workloadProxyingEnabled = setupWorkloadProxyingEnabledFeatureWatch()
const { data: featuresConfig } = useFeatures()
</script>
<template>
@ -118,7 +118,10 @@ const workloadProxyingEnabled = setupWorkloadProxyingEnabledFeatureWatch()
<TSidebarList :items="items" />
<ExposedServiceSideBar
v-if="workloadProxyingEnabled && cluster?.spec?.features?.enable_workload_proxy"
v-if="
featuresConfig?.spec.enable_workload_proxying &&
cluster?.spec?.features?.enable_workload_proxy
"
/>
<RouterView name="nodeSidebar" class="border-t border-naturals-n4" />

View File

@ -12,7 +12,7 @@ import {
} from '@/api/resources'
import TCheckbox from '@/components/common/Checkbox/TCheckbox.vue'
import Tooltip from '@/components/common/Tooltip/Tooltip.vue'
import { setupWorkloadProxyingEnabledFeatureWatch } from '@/methods/features'
import { useFeatures } from '@/methods/features'
type Props = {
disabled?: boolean
@ -21,11 +21,11 @@ type Props = {
defineProps<Props>()
const checked = defineModel<boolean>({ default: false })
const workloadProxyingEnabled = setupWorkloadProxyingEnabledFeatureWatch()
const { data: features } = useFeatures()
</script>
<template>
<Tooltip v-if="workloadProxyingEnabled" placement="bottom">
<Tooltip v-if="features?.spec.enable_workload_proxying" placement="bottom">
<template #description>
<div class="flex flex-col gap-1 p-2">
<p>Enable HTTP proxying to the Services in the cluster through Omni.</p>

View File

@ -29,6 +29,7 @@ import {
canReadClusters,
canReadMachines,
} from '@/methods/auth'
import { useFeatures } from '@/methods/features'
const machineMetrics = ref<Resource<MachineStatusMetricsSpec>>()
const machineMetricsWatch = new Watch(machineMetrics)
@ -92,6 +93,8 @@ const groupProviders = (
return res
}
const { data: featuresConfig } = useFeatures()
const items = computed(() => {
const result: SideBarItem[] = [
{
@ -202,31 +205,42 @@ const items = computed(() => {
}
if (canManageUsers.value || (backupStatus.value.configurable && canManageBackupStore.value)) {
const subItems: SideBarItem[] = [
{
name: 'Users',
route: getRoute('Users', '/settings/users'),
icon: 'users',
},
{
name: 'Service Accounts',
route: getRoute('ServiceAccounts', '/settings/serviceaccounts'),
icon: 'users',
},
{
name: 'Infra Providers',
route: getRoute('InfraProviders', '/settings/infraproviders'),
icon: 'machines-autoprovisioned',
},
{
name: 'Backups',
route: getRoute('Backups', '/settings/backups'),
icon: 'rollback',
},
]
if (featuresConfig.value?.spec.stripe_settings?.enabled) {
subItems.push({
name: 'Stripe',
route: 'https://billing.stripe.com/p/login/8wMcOC8z51GgdPi144',
regularLink: true,
icon: 'dashboard',
})
}
result.push({
name: 'Settings',
icon: 'settings',
subItems: [
{
name: 'Users',
route: getRoute('Users', '/settings/users'),
icon: 'users',
},
{
name: 'Service Accounts',
route: getRoute('ServiceAccounts', '/settings/serviceaccounts'),
icon: 'users',
},
{
name: 'Infra Providers',
route: getRoute('InfraProviders', '/settings/infraproviders'),
icon: 'machines-autoprovisioned',
},
{
name: 'Backups',
route: getRoute('Backups', '/settings/backups'),
icon: 'rollback',
},
],
subItems,
})
}

View File

@ -37,6 +37,9 @@ func UpdateResources(ctx context.Context, st state.State, logger *zap.Logger) er
res.TypedSpec().Value.UserPilotSettings = &specs.UserPilotSettings{
AppToken: config.Config.Account.UserPilot.AppToken,
}
res.TypedSpec().Value.StripeSettings = &specs.StripeSettings{
Enabled: config.Config.Logs.Stripe.Enabled,
}
return nil
}