|
9 | 9 | * by the Apache License, Version 2.0 |
10 | 10 | */ |
11 | 11 |
|
12 | | -import { Alert, AlertIcon, Button, DataTable, Result } from '@redpanda-data/ui'; |
| 12 | +import { useQuery } from '@connectrpc/connect-query'; |
| 13 | +import { Alert, AlertIcon, Button, DataTable, Result, Skeleton } from '@redpanda-data/ui'; |
13 | 14 | import { SkipIcon } from 'components/icons'; |
14 | | -import { computed, makeObservable } from 'mobx'; |
15 | | -import { observer } from 'mobx-react'; |
| 15 | +import { useMemo } from 'react'; |
16 | 16 |
|
17 | | -import { appGlobal } from '../../../state/app-global'; |
18 | | -import { api } from '../../../state/backend-api'; |
19 | | -import { type QuotaResponseSetting, QuotaType } from '../../../state/rest-interfaces'; |
20 | | -import { toJson } from '../../../utils/json-utils'; |
21 | | -import { DefaultSkeleton, InfoText } from '../../../utils/tsx-utils'; |
| 17 | +import { Quota_ValueType } from '../../../protogen/redpanda/api/dataplane/v1/quota_pb'; |
| 18 | +import { listQuotas } from '../../../protogen/redpanda/api/dataplane/v1/quota-QuotaService_connectquery'; |
| 19 | +import { InfoText } from '../../../utils/tsx-utils'; |
22 | 20 | import { prettyBytes, prettyNumber } from '../../../utils/utils'; |
23 | 21 | import PageContent from '../../misc/page-content'; |
24 | 22 | import Section from '../../misc/section'; |
25 | | -import { PageComponent, type PageInitHelper } from '../page'; |
26 | 23 |
|
27 | | -@observer |
28 | | -class QuotasList extends PageComponent { |
29 | | - constructor(p: Readonly<{ matchedPath: string }>) { |
30 | | - super(p); |
31 | | - makeObservable(this); |
32 | | - } |
| 24 | +const QuotasList = () => { |
| 25 | + const { data, error, isLoading } = useQuery(listQuotas, {}); |
33 | 26 |
|
34 | | - initPage(p: PageInitHelper): void { |
35 | | - p.title = 'Quotas'; |
36 | | - p.addBreadcrumb('Quotas', '/quotas'); |
| 27 | + const quotasData = useMemo(() => { |
| 28 | + if (!data?.quotas) return []; |
37 | 29 |
|
38 | | - this.refreshData(true); |
39 | | - appGlobal.onRefresh = () => this.refreshData(true); |
40 | | - } |
| 30 | + return data.quotas.map((entry) => { |
| 31 | + const entityType = entry.entity?.entityType; |
| 32 | + const entityName = entry.entity?.entityName; |
41 | 33 |
|
42 | | - refreshData(force: boolean) { |
43 | | - if (api.userData !== null && api.userData !== undefined && !api.userData.canListQuotas) { |
44 | | - return; |
45 | | - } |
46 | | - api.refreshQuotas(force); |
47 | | - } |
| 34 | + // Map entity type to display string |
| 35 | + let displayType: 'client-id' | 'user' | 'ip' | 'unknown' = 'unknown'; |
| 36 | + if (entityType === 1) displayType = 'client-id'; |
| 37 | + else if (entityType === 3) displayType = 'user'; |
| 38 | + else if (entityType === 4) displayType = 'ip'; |
48 | 39 |
|
49 | | - render() { |
50 | | - if (api.userData !== null && api.userData !== undefined && !api.userData.canListQuotas) { |
51 | | - return PermissionDenied; |
52 | | - } |
53 | | - if (api.Quotas === undefined) { |
54 | | - return DefaultSkeleton; |
55 | | - } |
| 40 | + return { |
| 41 | + eqKey: `${entityType}-${entityName}`, |
| 42 | + entityType: displayType, |
| 43 | + entityName: entityName || undefined, |
| 44 | + values: entry.values, |
| 45 | + }; |
| 46 | + }); |
| 47 | + }, [data]); |
56 | 48 |
|
57 | | - const warning = |
58 | | - api.Quotas === null ? ( |
59 | | - <Alert status="warning" style={{ marginBottom: '1em' }} variant="solid"> |
60 | | - <AlertIcon /> |
61 | | - You do not have the necessary permissions to view Quotas |
62 | | - </Alert> |
63 | | - ) : null; |
| 49 | + const formatBytes = (values: Quota_Value[], valueType: Quota_ValueType) => { |
| 50 | + const value = values.find((v) => v.valueType === valueType)?.value; |
| 51 | + return value ? ( |
| 52 | + prettyBytes(value) |
| 53 | + ) : ( |
| 54 | + <span style={{ opacity: 0.3 }}> |
| 55 | + <SkipIcon /> |
| 56 | + </span> |
| 57 | + ); |
| 58 | + }; |
64 | 59 |
|
65 | | - const resources = this.quotasList; |
66 | | - const formatBytes = (x: undefined | number) => |
67 | | - x ? ( |
68 | | - prettyBytes(x) |
69 | | - ) : ( |
70 | | - <span style={{ opacity: 0.3 }}> |
71 | | - <SkipIcon /> |
72 | | - </span> |
73 | | - ); |
74 | | - const formatRate = (x: undefined | number) => |
75 | | - x ? ( |
76 | | - prettyNumber(x) |
77 | | - ) : ( |
78 | | - <span style={{ opacity: 0.3 }}> |
79 | | - <SkipIcon /> |
80 | | - </span> |
81 | | - ); |
| 60 | + const formatRate = (values: (typeof quotasData)[0]['values'], valueType: Quota_ValueType) => { |
| 61 | + const value = values.find((v) => v.valueType === valueType)?.value; |
| 62 | + return value ? ( |
| 63 | + prettyNumber(value) |
| 64 | + ) : ( |
| 65 | + <span style={{ opacity: 0.3 }}> |
| 66 | + <SkipIcon /> |
| 67 | + </span> |
| 68 | + ); |
| 69 | + }; |
82 | 70 |
|
| 71 | + if (isLoading) { |
83 | 72 | return ( |
84 | 73 | <PageContent> |
85 | 74 | <Section> |
86 | | - {warning} |
87 | | - |
88 | | - <DataTable<{ |
89 | | - eqKey: string; |
90 | | - entityType: 'client-id' | 'user' | 'ip'; |
91 | | - entityName?: string | undefined; |
92 | | - settings: QuotaResponseSetting[]; |
93 | | - }> |
94 | | - columns={[ |
95 | | - { |
96 | | - size: 100, // Assuming '100px' translates to '100' |
97 | | - header: 'Type', |
98 | | - accessorKey: 'entityType', |
99 | | - }, |
100 | | - { |
101 | | - size: 100, // 'auto' width replaced with an example number |
102 | | - header: 'Name', |
103 | | - accessorKey: 'entityName', |
104 | | - }, |
105 | | - { |
106 | | - size: 100, |
107 | | - header: () => <InfoText tooltip="Limit throughput of produce requests">Producer Rate</InfoText>, |
108 | | - accessorKey: 'producerRate', |
109 | | - cell: ({ row: { original } }) => |
110 | | - formatBytes(original.settings.first((k) => k.key === QuotaType.PRODUCER_BYTE_RATE)?.value), |
111 | | - }, |
112 | | - { |
113 | | - size: 100, |
114 | | - header: () => <InfoText tooltip="Limit throughput of fetch requests">Consumer Rate</InfoText>, |
115 | | - accessorKey: 'consumerRate', |
116 | | - cell: ({ row: { original } }) => |
117 | | - formatBytes(original.settings.first((k) => k.key === QuotaType.CONSUMER_BYTE_RATE)?.value), |
118 | | - }, |
119 | | - { |
120 | | - size: 100, |
121 | | - header: () => ( |
122 | | - <InfoText tooltip="Limit rate of topic mutation requests, including create, add, and delete partition, in number of partitions per second"> |
123 | | - Controller Mutation Rate |
124 | | - </InfoText> |
125 | | - ), |
126 | | - accessorKey: 'controllerMutationRate', |
127 | | - cell: ({ row: { original } }) => |
128 | | - formatRate(original.settings.first((k) => k.key === QuotaType.CONTROLLER_MUTATION_RATE)?.value), |
129 | | - }, |
130 | | - ]} |
131 | | - data={resources} |
132 | | - /> |
| 75 | + <Skeleton height="400px" /> |
133 | 76 | </Section> |
134 | 77 | </PageContent> |
135 | 78 | ); |
136 | 79 | } |
137 | 80 |
|
138 | | - @computed get quotasList() { |
139 | | - const quotaResponse = api.Quotas; |
140 | | - if (!quotaResponse || quotaResponse.error) { |
141 | | - return []; |
| 81 | + if (error) { |
| 82 | + const isPermissionError = error.message.includes('permission') || error.message.includes('forbidden'); |
| 83 | + |
| 84 | + if (isPermissionError) { |
| 85 | + return ( |
| 86 | + <PageContent> |
| 87 | + <Section> |
| 88 | + <Result |
| 89 | + extra={ |
| 90 | + <a href="https://docs.redpanda.com/docs/manage/console/" rel="noopener noreferrer" target="_blank"> |
| 91 | + <Button variant="solid">Redpanda Console documentation for roles and permissions</Button> |
| 92 | + </a> |
| 93 | + } |
| 94 | + status={403} |
| 95 | + title="Forbidden" |
| 96 | + userMessage={ |
| 97 | + <p> |
| 98 | + You are not allowed to view this page. |
| 99 | + <br /> |
| 100 | + Contact the administrator if you think this is an error. |
| 101 | + </p> |
| 102 | + } |
| 103 | + /> |
| 104 | + </Section> |
| 105 | + </PageContent> |
| 106 | + ); |
142 | 107 | } |
143 | 108 |
|
144 | | - return quotaResponse.items.map((x) => ({ ...x, eqKey: toJson(x) })); |
| 109 | + return ( |
| 110 | + <PageContent> |
| 111 | + <Section> |
| 112 | + <Alert status="warning" style={{ marginBottom: '1em' }} variant="solid"> |
| 113 | + <AlertIcon /> |
| 114 | + {error.message || 'Failed to load quotas'} |
| 115 | + </Alert> |
| 116 | + </Section> |
| 117 | + </PageContent> |
| 118 | + ); |
145 | 119 | } |
146 | | -} |
147 | 120 |
|
148 | | -const PermissionDenied = ( |
149 | | - <> |
150 | | - <PageContent key="quotasNoPerms"> |
| 121 | + return ( |
| 122 | + <PageContent> |
151 | 123 | <Section> |
152 | | - <Result |
153 | | - extra={ |
154 | | - <a href="https://docs.redpanda.com/docs/manage/console/" rel="noopener noreferrer" target="_blank"> |
155 | | - <Button variant="solid">Redpanda Console documentation for roles and permissions</Button> |
156 | | - </a> |
157 | | - } |
158 | | - status={403} |
159 | | - title="Forbidden" |
160 | | - userMessage={ |
161 | | - <p> |
162 | | - You are not allowed to view this page. |
163 | | - <br /> |
164 | | - Contact the administrator if you think this is an error. |
165 | | - </p> |
166 | | - } |
| 124 | + <DataTable<{ |
| 125 | + eqKey: string; |
| 126 | + entityType: 'client-id' | 'user' | 'ip' | 'unknown'; |
| 127 | + entityName?: string | undefined; |
| 128 | + values: Array<{ valueType: Quota_ValueType; value: number }>; |
| 129 | + }> |
| 130 | + columns={[ |
| 131 | + { |
| 132 | + size: 100, |
| 133 | + header: 'Type', |
| 134 | + accessorKey: 'entityType', |
| 135 | + }, |
| 136 | + { |
| 137 | + size: 100, |
| 138 | + header: 'Name', |
| 139 | + accessorKey: 'entityName', |
| 140 | + }, |
| 141 | + { |
| 142 | + size: 100, |
| 143 | + header: () => <InfoText tooltip="Limit throughput of produce requests">Producer Rate</InfoText>, |
| 144 | + accessorKey: 'producerRate', |
| 145 | + cell: ({ row: { original } }) => formatBytes(original.values, Quota_ValueType.PRODUCER_BYTE_RATE), |
| 146 | + }, |
| 147 | + { |
| 148 | + size: 100, |
| 149 | + header: () => <InfoText tooltip="Limit throughput of fetch requests">Consumer Rate</InfoText>, |
| 150 | + accessorKey: 'consumerRate', |
| 151 | + cell: ({ row: { original } }) => formatBytes(original.values, Quota_ValueType.CONSUMER_BYTE_RATE), |
| 152 | + }, |
| 153 | + { |
| 154 | + size: 100, |
| 155 | + header: () => ( |
| 156 | + <InfoText tooltip="Limit rate of topic mutation requests, including create, add, and delete partition, in number of partitions per second"> |
| 157 | + Controller Mutation Rate |
| 158 | + </InfoText> |
| 159 | + ), |
| 160 | + accessorKey: 'controllerMutationRate', |
| 161 | + cell: ({ row: { original } }) => formatRate(original.values, Quota_ValueType.CONTROLLER_MUTATION_RATE), |
| 162 | + }, |
| 163 | + ]} |
| 164 | + data={quotasData} |
167 | 165 | /> |
168 | 166 | </Section> |
169 | 167 | </PageContent> |
170 | | - </> |
171 | | -); |
| 168 | + ); |
| 169 | +}; |
172 | 170 |
|
173 | 171 | export default QuotasList; |
0 commit comments