|
1 |
| -import { Table, Button, notification, Typography, Tooltip, Spin } from 'antd' |
| 1 | +import { Table, Button, notification, Typography, Tooltip, Spin, Select, Space } from 'antd' |
2 | 2 | import { usePollingEffect } from '../../utils/usePollingEffect'
|
3 |
| -import React, { useState } from 'react' |
| 3 | +import React, { useState, useEffect } from 'react' |
4 | 4 | import { ColumnType } from 'antd/es/table'
|
5 | 5 |
|
6 | 6 | const { Paragraph } = Typography
|
7 | 7 |
|
8 |
| -interface RunningQueryData { |
9 |
| - query: string |
10 |
| - read_rows: number |
11 |
| - read_rows_readable: string |
12 |
| - query_id: string |
13 |
| - total_rows_approx: number |
14 |
| - total_rows_approx_readable: string |
15 |
| - elapsed: number |
16 |
| - memory_usage: string |
| 8 | +interface ClusterNode { |
| 9 | + cluster: string |
| 10 | + shard_num: number |
| 11 | + shard_weight: number |
| 12 | + replica_num: number |
| 13 | + host_name: string |
| 14 | + host_address: string |
| 15 | + port: number |
| 16 | + is_local: number |
| 17 | + user: string |
| 18 | + default_database: string |
| 19 | + errors_count: number |
| 20 | + slowdowns_count: number |
| 21 | + estimated_recovery_time: number |
17 | 22 | }
|
18 | 23 |
|
19 |
| -function KillQueryButton({ queryId }: any) { |
20 |
| - const [isLoading, setIsLoading] = useState(false) |
21 |
| - const [isKilled, setIsKilled] = useState(false) |
| 24 | +interface Cluster { |
| 25 | + cluster: string |
| 26 | + nodes: ClusterNode[] |
| 27 | +} |
22 | 28 |
|
23 |
| - const killQuery = async () => { |
24 |
| - setIsLoading(true) |
25 |
| - try { |
26 |
| - const res = await fetch(`/api/analyze/${queryId}/kill_query`, { |
27 |
| - method: 'POST', |
28 |
| - headers: { |
29 |
| - 'Content-Type': 'application/x-www-form-urlencoded', |
30 |
| - }, |
31 |
| - body: new URLSearchParams({ |
32 |
| - query_id: queryId, |
33 |
| - }), |
34 |
| - }) |
35 |
| - setIsKilled(true) |
36 |
| - setIsLoading(false) |
37 |
| - return await res.json() |
38 |
| - } catch (err) { |
39 |
| - setIsLoading(false) |
40 |
| - notification.error({ |
41 |
| - message: 'Killing query failed', |
42 |
| - }) |
43 |
| - } |
44 |
| - } |
45 |
| - return ( |
46 |
| - <> |
47 |
| - {isKilled ? ( |
48 |
| - <Button disabled>Query killed</Button> |
49 |
| - ) : ( |
50 |
| - <Button danger onClick={killQuery} loading={isLoading}> |
51 |
| - Kill query |
52 |
| - </Button> |
53 |
| - )} |
54 |
| - </> |
55 |
| - ) |
| 29 | +interface ReplicationQueueItem { |
| 30 | + host_name: string |
| 31 | + database: string |
| 32 | + table: string |
| 33 | + position: number |
| 34 | + error: string |
| 35 | + last_attempt_time: string |
| 36 | + num_attempts: number |
| 37 | + type: string |
56 | 38 | }
|
57 | 39 |
|
58 | 40 | export default function Replication() {
|
59 |
| - const [runningQueries, setRunningQueries] = useState([]) |
60 |
| - const [loadingRunningQueries, setLoadingRunningQueries] = useState(false) |
| 41 | + const [replicationQueue, setReplicationQueue] = useState<ReplicationQueueItem[]>([]) |
| 42 | + const [loadingReplication, setLoadingReplication] = useState(false) |
| 43 | + const [selectedCluster, setSelectedCluster] = useState<string>('') |
| 44 | + const [clusters, setClusters] = useState<Cluster[]>([]) |
| 45 | + const [loadingClusters, setLoadingClusters] = useState(false) |
| 46 | + |
| 47 | + useEffect(() => { |
| 48 | + const fetchClusters = async () => { |
| 49 | + setLoadingClusters(true) |
| 50 | + try { |
| 51 | + const res = await fetch('/api/clusters') |
| 52 | + const resJson: Cluster[] = await res.json() |
| 53 | + setClusters(resJson) |
| 54 | + if (resJson.length > 0) { |
| 55 | + setSelectedCluster(resJson[0].cluster) |
| 56 | + } |
| 57 | + } catch (err) { |
| 58 | + notification.error({ |
| 59 | + message: 'Failed to fetch clusters', |
| 60 | + description: 'Please try again later', |
| 61 | + }) |
| 62 | + } |
| 63 | + setLoadingClusters(false) |
| 64 | + } |
| 65 | + fetchClusters() |
| 66 | + }, []) |
61 | 67 |
|
62 |
| - const columns: ColumnType<RunningQueryData>[] = [ |
| 68 | + const columns: ColumnType<ReplicationQueueItem>[] = [ |
| 69 | + { |
| 70 | + title: 'Host', |
| 71 | + dataIndex: 'host_name', |
| 72 | + key: 'host_name', |
| 73 | + }, |
63 | 74 | {
|
64 |
| - title: 'Query', |
65 |
| - dataIndex: 'normalized_query', |
66 |
| - key: 'query', |
67 |
| - render: (_: any, item) => { |
68 |
| - let index = 0 |
69 |
| - return ( |
70 |
| - <Paragraph |
71 |
| - style={{ maxWidth: '100%', fontFamily: 'monospace' }} |
72 |
| - ellipsis={{ |
73 |
| - rows: 2, |
74 |
| - expandable: true, |
75 |
| - }} |
76 |
| - > |
77 |
| - {item.query.replace(/(\?)/g, () => { |
78 |
| - index = index + 1 |
79 |
| - return '$' + index |
80 |
| - })} |
81 |
| - </Paragraph> |
82 |
| - ) |
83 |
| - }, |
| 75 | + title: 'Database', |
| 76 | + dataIndex: 'database', |
| 77 | + key: 'database', |
84 | 78 | },
|
85 |
| - { title: 'User', dataIndex: 'user' }, |
86 |
| - { title: 'Elapsed time', dataIndex: 'elapsed' }, |
87 | 79 | {
|
88 |
| - title: 'Rows read', |
89 |
| - dataIndex: 'read_rows', |
90 |
| - render: (_: any, item) => ( |
91 |
| - <Tooltip title={`~${item.read_rows}/${item.total_rows_approx}`}> |
92 |
| - ~{item.read_rows_readable}/{item.total_rows_approx_readable} |
93 |
| - </Tooltip> |
| 80 | + title: 'Table', |
| 81 | + dataIndex: 'table', |
| 82 | + key: 'table', |
| 83 | + }, |
| 84 | + { |
| 85 | + title: 'Error', |
| 86 | + dataIndex: 'error', |
| 87 | + key: 'error', |
| 88 | + render: (error: string) => ( |
| 89 | + <Paragraph |
| 90 | + style={{ maxWidth: '400px', color: 'red' }} |
| 91 | + ellipsis={{ |
| 92 | + rows: 2, |
| 93 | + expandable: true, |
| 94 | + }} |
| 95 | + > |
| 96 | + {error} |
| 97 | + </Paragraph> |
94 | 98 | ),
|
95 | 99 | },
|
96 |
| - { title: 'Memory Usage', dataIndex: 'memory_usage' }, |
97 | 100 | {
|
98 |
| - title: 'Actions', |
99 |
| - render: (_: any, item) => <KillQueryButton queryId={item.query_id} />, |
| 101 | + title: 'Last Attempt', |
| 102 | + dataIndex: 'last_attempt_time', |
| 103 | + key: 'last_attempt_time', |
| 104 | + }, |
| 105 | + { |
| 106 | + title: 'Attempts', |
| 107 | + dataIndex: 'num_attempts', |
| 108 | + key: 'num_attempts', |
| 109 | + }, |
| 110 | + { |
| 111 | + title: 'Type', |
| 112 | + dataIndex: 'type', |
| 113 | + key: 'type', |
100 | 114 | },
|
101 | 115 | ]
|
102 | 116 |
|
103 | 117 | usePollingEffect(
|
104 | 118 | async () => {
|
105 |
| - setLoadingRunningQueries(true) |
106 |
| - const res = await fetch('/api/analyze/running_queries') |
107 |
| - const resJson = await res.json() |
108 |
| - setRunningQueries(resJson) |
109 |
| - setLoadingRunningQueries(false) |
| 119 | + if (!selectedCluster) return |
| 120 | + |
| 121 | + setLoadingReplication(true) |
| 122 | + try { |
| 123 | + const res = await fetch(`/api/replication/?cluster=${selectedCluster}`) |
| 124 | + const resJson = await res.json() |
| 125 | + // Filter for failed items only |
| 126 | + const failedItems = resJson.filter((item: ReplicationQueueItem) => item.error) |
| 127 | + setReplicationQueue(failedItems) |
| 128 | + } catch (err) { |
| 129 | + notification.error({ |
| 130 | + message: 'Failed to fetch replication queue', |
| 131 | + description: 'Please try again later', |
| 132 | + }) |
| 133 | + } |
| 134 | + setLoadingReplication(false) |
110 | 135 | },
|
111 |
| - [], |
| 136 | + [selectedCluster], |
112 | 137 | { interval: 5000 }
|
113 | 138 | )
|
114 | 139 |
|
115 | 140 | return (
|
116 | 141 | <>
|
117 |
| - <h1 style={{ textAlign: 'left' }}>Running queries {loadingRunningQueries ? <Spin /> : null}</h1> |
118 |
| - <br /> |
119 |
| - <Table |
120 |
| - columns={columns} |
121 |
| - dataSource={runningQueries} |
122 |
| - loading={runningQueries.length == 0 && loadingRunningQueries} |
123 |
| - /> |
| 142 | + <Space direction="vertical" size="large" style={{ width: '100%' }}> |
| 143 | + <Space> |
| 144 | + <h1 style={{ margin: 0 }}> |
| 145 | + {`${replicationQueue.length}`} Failed Replication Queue Items |
| 146 | + </h1> |
| 147 | + {loadingReplication && <Spin />} |
| 148 | + </Space> |
| 149 | + |
| 150 | + <Select |
| 151 | + style={{ width: 200 }} |
| 152 | + value={selectedCluster} |
| 153 | + onChange={setSelectedCluster} |
| 154 | + loading={loadingClusters} |
| 155 | + placeholder="Select a cluster" |
| 156 | + > |
| 157 | + {clusters.map((cluster) => ( |
| 158 | + <Select.Option key={cluster.cluster} value={cluster.cluster}> |
| 159 | + {cluster.cluster} |
| 160 | + </Select.Option> |
| 161 | + ))} |
| 162 | + </Select> |
| 163 | + |
| 164 | + <Table |
| 165 | + columns={columns} |
| 166 | + dataSource={replicationQueue} |
| 167 | + loading={replicationQueue.length === 0 && loadingReplication} |
| 168 | + rowKey={(record) => `${record.host_name}-${record.table}-${record.position}`} |
| 169 | + /> |
| 170 | + </Space> |
124 | 171 | </>
|
125 | 172 | )
|
126 | 173 | }
|
0 commit comments