ydb-embedded-ui 1.1.3 → 1.2.2

Sign up to get free protection for your applications and to get access to all the features.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ### [1.2.2](https://www.github.com/ydb-platform/ydb-embedded-ui/compare/v1.2.1...v1.2.2) (2022-05-04)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * code-review ([288fda3](https://www.github.com/ydb-platform/ydb-embedded-ui/commit/288fda3cd207908e9b5c0486c4d486c6f2e17dd4))
9
+ * reducer clusterInfo should not be used ([1cafcbf](https://www.github.com/ydb-platform/ydb-embedded-ui/commit/1cafcbfb15f668b100cf6628b540b7cd234f6024))
10
+
11
+ ### [1.2.1](https://www.github.com/ydb-platform/ydb-embedded-ui/compare/v1.2.0...v1.2.1) (2022-04-27)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * **Vdisk:** should not fail if no node id passed ([d66686d](https://www.github.com/ydb-platform/ydb-embedded-ui/commit/d66686d0cbd9f61c4e106f6775db2fca226c922f))
17
+
18
+ ## [1.2.0](https://www.github.com/ydb-platform/ydb-embedded-ui/compare/v1.1.3...v1.2.0) (2022-04-26)
19
+
20
+
21
+ ### Features
22
+
23
+ * **Storage:** smoother loading state for storage table ([f7f38c4](https://www.github.com/ydb-platform/ydb-embedded-ui/commit/f7f38c455dd9abc3f898048081e90af9b460f922))
24
+
25
+
26
+ ### Bug Fixes
27
+
28
+ * prevent ghost autofetch ([153d829](https://www.github.com/ydb-platform/ydb-embedded-ui/commit/153d8291d315f1dab001a69981a12e30d3d2aea9))
29
+
3
30
  ### [1.1.3](https://www.github.com/ydb-platform/ydb-embedded-ui/compare/v1.1.2...v1.1.3) (2022-04-20)
4
31
 
5
32
 
@@ -0,0 +1,38 @@
1
+ .table-skeleton {
2
+ width: 100%;
3
+
4
+ &__row {
5
+ display: flex;
6
+ align-items: center;
7
+
8
+ height: var(--data-table-row-height);
9
+
10
+ .yc-skeleton {
11
+ height: var(--yc-text-body2-line-height);
12
+ }
13
+ }
14
+
15
+ &__col-1 {
16
+ width: 10%;
17
+ margin-right: 5%;
18
+ }
19
+
20
+ &__col-2 {
21
+ width: 7%;
22
+ margin-right: 5%;
23
+ }
24
+
25
+ &__col-3,
26
+ &__col-4 {
27
+ width: 5%;
28
+ margin-right: 5%;
29
+ }
30
+
31
+ &__col-5 {
32
+ width: 20%;
33
+ }
34
+
35
+ &__col-full {
36
+ width: 100%;
37
+ }
38
+ }
@@ -0,0 +1,29 @@
1
+ import { FC } from 'react';
2
+ import block from 'bem-cn-lite';
3
+ import { Skeleton } from '@yandex-cloud/uikit';
4
+
5
+ import './TableSkeleton.scss';
6
+
7
+ const b = block('table-skeleton');
8
+
9
+ interface TableSkeletonProps {
10
+ className?: string;
11
+ rows?: number;
12
+ }
13
+
14
+ export const TableSkeleton: FC<TableSkeletonProps> = ({ rows = 2, className }) => (
15
+ <div className={b(null, className)}>
16
+ <div className={b('row')}>
17
+ <Skeleton className={b('col-1')} />
18
+ <Skeleton className={b('col-2')} />
19
+ <Skeleton className={b('col-3')} />
20
+ <Skeleton className={b('col-4')} />
21
+ <Skeleton className={b('col-5')} />
22
+ </div>
23
+ {[...new Array(rows)].map((_, index) => (
24
+ <div className={b('row')} key={`skeleton-row-${index}`}>
25
+ <Skeleton className={b('col-full')} />
26
+ </div>
27
+ ))}
28
+ </div>
29
+ );
@@ -17,6 +17,8 @@ registerLanguages();
17
17
  class App extends React.Component {
18
18
  static propTypes = {
19
19
  isAuthenticated: PropTypes.bool,
20
+ singleClusterMode: PropTypes.bool,
21
+ clusterName: PropTypes.string,
20
22
  children: PropTypes.node,
21
23
  };
22
24
 
@@ -40,9 +42,10 @@ class App extends React.Component {
40
42
  }
41
43
 
42
44
  renderContentWithNavigation() {
45
+ const {singleClusterMode, clusterName} = this.props;
43
46
  return (
44
47
  <AsideNavigation>
45
- <Content singleClusterMode={this.props.singleClusterMode} />
48
+ <Content singleClusterMode={singleClusterMode} clusterName={clusterName} />
46
49
  <div id="fullscreen-root"></div>
47
50
  </AsideNavigation>
48
51
  );
@@ -58,6 +61,7 @@ function mapStateToProps(state) {
58
61
  isAuthenticated: state.authentication.isAuthenticated,
59
62
  internalUser: state.authentication.user,
60
63
  singleClusterMode: state.singleClusterMode,
64
+ clusterName: state.cluster.data?.Name,
61
65
  };
62
66
  }
63
67
 
@@ -59,7 +59,7 @@ export function Content(props) {
59
59
  };
60
60
  return (
61
61
  <React.Fragment>
62
- {!isClustersPage && <Header />}
62
+ {!isClustersPage && <Header clusterName={props.clusterName} />}
63
63
  <main className={b('main')}>{renderRoute()}</main>
64
64
  <ReduxTooltip />
65
65
  <AppIcons />
@@ -70,6 +70,7 @@ export function Content(props) {
70
70
  Content.propTypes = {
71
71
  singleClusterMode: PropTypes.bool,
72
72
  children: PropTypes.node,
73
+ clusterName: PropTypes.string,
73
74
  };
74
75
 
75
76
  function ContentWrapper(props) {
@@ -26,16 +26,16 @@ function ClusterName({name}: {name: string}) {
26
26
  );
27
27
  }
28
28
 
29
- function Header() {
29
+ interface HeaderProps {
30
+ clusterName: string
31
+ }
32
+
33
+ function Header({clusterName}: HeaderProps) {
30
34
  const dispatch = useDispatch();
31
35
  const {data: host}: {data: {ClusterName?: string}} = useSelector((state: any) => state.host);
32
36
  const {singleClusterMode, header}: {singleClusterMode: boolean; header: HeaderItemType[]} =
33
37
  useSelector((state: any) => state);
34
38
 
35
- const clusterName: string = useSelector(
36
- (state: any) => state.cluster.data?.Name || state.clusterInfo.title,
37
- );
38
-
39
39
  const location = useLocation();
40
40
  const history = useHistory();
41
41
 
@@ -3,10 +3,11 @@ import PropTypes from 'prop-types';
3
3
  import {connect} from 'react-redux';
4
4
  import cn from 'bem-cn-lite';
5
5
  import DataTable from '@yandex-cloud/react-data-table';
6
- import {Loader, RadioButton, Label} from '@yandex-cloud/uikit';
6
+ import {RadioButton, Label} from '@yandex-cloud/uikit';
7
7
 
8
8
  import StorageFilter from './StorageFilter/StorageFilter';
9
9
  import {AutoFetcher} from '../../utils/autofetcher';
10
+ import {TableSkeleton} from '../../components/TableSkeleton/TableSkeleton';
10
11
 
11
12
  import {
12
13
  getStorageInfo,
@@ -68,17 +69,14 @@ class Storage extends React.Component {
68
69
  storageType,
69
70
  setHeader,
70
71
  getNodesList,
71
- getStorageInfo,
72
72
  } = this.props;
73
73
 
74
74
  this.autofetcher = new AutoFetcher();
75
75
  getNodesList();
76
76
  if (tenant || nodeId) {
77
77
  setVisibleEntities(VisibleEntities.All);
78
- getStorageInfo({
79
- tenant,
78
+ this.getStorageInfo({
80
79
  filter: FILTER_OPTIONS.All,
81
- nodeId,
82
80
  type: storageType,
83
81
  });
84
82
  } else {
@@ -88,16 +86,12 @@ class Storage extends React.Component {
88
86
  link: createHref(routes.cluster, {activeTab: CLUSTER_PAGES.storage.id}),
89
87
  },
90
88
  ]);
91
- getStorageInfo({
92
- tenant,
93
- nodeId,
89
+ this.getStorageInfo({
94
90
  filter: FILTER_OPTIONS.Missing,
95
91
  type: storageType,
96
92
  });
97
93
  this.autofetcher.fetch(() =>
98
- getStorageInfo({
99
- tenant,
100
- nodeId,
94
+ this.getStorageInfo({
101
95
  filter: FILTER_OPTIONS.Missing,
102
96
  type: storageType,
103
97
  }),
@@ -107,30 +101,23 @@ class Storage extends React.Component {
107
101
 
108
102
  componentDidUpdate(prevProps) {
109
103
  const {
110
- tenant,
111
104
  visibleEntities,
112
- getStorageInfo,
113
- nodeId,
114
105
  storageType,
115
106
  autorefresh,
116
107
  database,
117
108
  } = this.props;
118
109
 
119
110
  const startFetch = () => {
120
- getStorageInfo({
121
- tenant,
111
+ this.getStorageInfo({
122
112
  filter: FILTER_OPTIONS[visibleEntities],
123
- nodeId,
124
113
  type: storageType,
125
114
  });
126
115
 
127
116
  this.autofetcher.stop();
128
117
  this.autofetcher.start();
129
118
  this.autofetcher.fetch(() =>
130
- getStorageInfo({
131
- tenant,
119
+ this.getStorageInfo({
132
120
  filter: FILTER_OPTIONS[visibleEntities],
133
- nodeId,
134
121
  type: storageType,
135
122
  }),
136
123
  );
@@ -157,11 +144,25 @@ class Storage extends React.Component {
157
144
  this.props.setInitialState();
158
145
  }
159
146
 
147
+ getStorageInfo(data) {
148
+ const {
149
+ tenant,
150
+ nodeId,
151
+ getStorageInfo,
152
+ } = this.props;
153
+
154
+ getStorageInfo({
155
+ tenant,
156
+ nodeId,
157
+ ...data,
158
+ }, {
159
+ concurrentId: 'getStorageInfo',
160
+ });
161
+ }
162
+
160
163
  renderLoader() {
161
164
  return (
162
- <div className={b('loader')}>
163
- <Loader size="m" />
164
- </div>
165
+ <TableSkeleton className={b('loader')}/>
165
166
  );
166
167
  }
167
168
 
@@ -200,8 +201,9 @@ class Storage extends React.Component {
200
201
  };
201
202
 
202
203
  renderControls() {
203
- const {setStorageFilter, visibleEntities, storageType, flatListStorageEntities} =
204
+ const {setStorageFilter, visibleEntities, storageType, flatListStorageEntities, loading, wasLoaded} =
204
205
  this.props;
206
+ const showLoader = loading && !wasLoaded;
205
207
  return (
206
208
  <div className={b('controls')}>
207
209
  <div className={b('search')}>
@@ -232,25 +234,28 @@ class Storage extends React.Component {
232
234
  </RadioButton>
233
235
  <Label theme="info" size="m">{`${
234
236
  storageType === StorageTypes.groups ? 'Groups' : 'Nodes'
235
- }: ${flatListStorageEntities.length}`}</Label>
237
+ }: ${(showLoader) ? '...' : flatListStorageEntities.length}`}</Label>
236
238
  </div>
237
239
  );
238
240
  }
239
241
 
240
242
  render() {
241
243
  const {loading, wasLoaded, error} = this.props;
242
- if (loading && !wasLoaded) {
243
- return this.renderLoader();
244
- } else if (error) {
245
- return <div>{error.statusText}</div>;
246
- } else {
247
- return (
248
- <div className={b()}>
249
- {this.renderControls()}
250
- {this.renderDataTable()}
251
- </div>
252
- );
253
- }
244
+ const showLoader = loading && !wasLoaded;
245
+
246
+ return (
247
+ <div className={b()}>
248
+ {this.renderControls()}
249
+ {error && (
250
+ <div>{error.statusText}</div>
251
+ )}
252
+ {showLoader ? (
253
+ this.renderLoader()
254
+ ) : (
255
+ this.renderDataTable()
256
+ )}
257
+ </div>
258
+ );
254
259
  }
255
260
  }
256
261
 
@@ -43,12 +43,6 @@
43
43
  }
44
44
  }
45
45
 
46
- &__loader {
47
- display: flex;
48
- justify-content: center;
49
-
50
- margin-top: 100px;
51
- }
52
46
  .entity-status {
53
47
  justify-content: center;
54
48
  }
@@ -235,11 +235,18 @@ function Vdisk(props) {
235
235
  <DiskStateProgressBar
236
236
  diskAllocatedPercent={vdiskAllocatedPercent}
237
237
  severity={severity}
238
- href={createHref(
239
- routes.node,
240
- {id: props.NodeId, activeTab: STRUCTURE},
241
- {pdiskId: props.PDisk?.PDiskId, vdiskId: stringifyVdiskId(props.VDiskId)},
242
- )}
238
+ href={
239
+ props.NodeId
240
+ ? createHref(
241
+ routes.node,
242
+ {id: props.NodeId, activeTab: STRUCTURE},
243
+ {
244
+ pdiskId: props.PDisk?.PDiskId,
245
+ vdiskId: stringifyVdiskId(props.VDiskId),
246
+ },
247
+ )
248
+ : undefined
249
+ }
243
250
  />
244
251
  </div>
245
252
  </React.Fragment>
@@ -35,7 +35,7 @@ export class YdbEmbeddedAPI extends AxiosWrapper {
35
35
  storage: true,
36
36
  });
37
37
  }
38
- getStorageInfo({tenant, filter, nodeId, type}) {
38
+ getStorageInfo({tenant, filter, nodeId, type}, {concurrentId}) {
39
39
  return this.get(
40
40
  this.getPath(
41
41
  `/viewer/json/${type === StorageTypes.nodes ? 'nodes' : 'storage'}?enums=true`,
@@ -45,6 +45,9 @@ export class YdbEmbeddedAPI extends AxiosWrapper {
45
45
  node_id: nodeId,
46
46
  with: filter,
47
47
  },
48
+ {
49
+ concurrentId,
50
+ },
48
51
  );
49
52
  }
50
53
  getPdiskInfo(nodeId, pdiskId) {
@@ -20,7 +20,6 @@ import tablet from './tablet';
20
20
  import executeQuery from './executeQuery';
21
21
  import explainQuery from './explainQuery';
22
22
  import tabletsFilters from './tabletsFilters';
23
- import clusterInfo from './clusterInfo';
24
23
  import settings from './settings';
25
24
  import preview from './preview';
26
25
  import nodesList from './clusterNodes';
@@ -63,7 +62,6 @@ export const rootReducer = {
63
62
  explainQuery,
64
63
  tabletsFilters,
65
64
  heatmap,
66
- clusterInfo,
67
65
  settings,
68
66
  preview,
69
67
  nodesList,
@@ -74,6 +74,8 @@ const storage = function z(state = initialState, action) {
74
74
  return {
75
75
  ...state,
76
76
  visible: action.data,
77
+ wasLoaded: false,
78
+ error: undefined,
77
79
  };
78
80
  }
79
81
  case SET_STORAGE_TYPE: {
@@ -81,6 +83,7 @@ const storage = function z(state = initialState, action) {
81
83
  ...state,
82
84
  type: action.data,
83
85
  wasLoaded: false,
86
+ error: undefined,
84
87
  };
85
88
  }
86
89
  default:
@@ -94,9 +97,9 @@ export function setInitialState() {
94
97
  };
95
98
  }
96
99
 
97
- export function getStorageInfo({tenant, filter, nodeId, type}) {
100
+ export function getStorageInfo({tenant, filter, nodeId, type}, {concurrentId}) {
98
101
  return createApiRequest({
99
- request: window.api.getStorageInfo({tenant, filter, nodeId, type}),
102
+ request: window.api.getStorageInfo({tenant, filter, nodeId, type}, {concurrentId}),
100
103
  actions: FETCH_STORAGE,
101
104
  });
102
105
  }
@@ -3,6 +3,7 @@ export class AutoFetcher {
3
3
  this.timeout = AutoFetcher.DEFAULT_TIMEOUT;
4
4
  this.active = true;
5
5
  this.timer = undefined;
6
+ this.launchCounter = 0;
6
7
  }
7
8
 
8
9
  wait(ms: number) {
@@ -16,6 +17,8 @@ export class AutoFetcher {
16
17
  return;
17
18
  }
18
19
 
20
+ const currentLaunch = this.launchCounter;
21
+
19
22
  await this.wait(this.timeout);
20
23
 
21
24
  if (this.active) {
@@ -23,6 +26,12 @@ export class AutoFetcher {
23
26
  await request();
24
27
  const finishTs = Date.now();
25
28
 
29
+ if (currentLaunch !== this.launchCounter) {
30
+ // autofetcher was restarted while request was in progress
31
+ // stop further fetches, we are in deprecated thread
32
+ return;
33
+ }
34
+
26
35
  const responseTime = finishTs - startTs;
27
36
  const nextTimeout =
28
37
  responseTime > AutoFetcher.MIN_TIMEOUT ? responseTime : AutoFetcher.MIN_TIMEOUT;
@@ -40,6 +49,7 @@ export class AutoFetcher {
40
49
  this.active = false;
41
50
  }
42
51
  start() {
52
+ this.launchCounter++;
43
53
  this.active = true;
44
54
  }
45
55
 
@@ -48,4 +58,5 @@ export class AutoFetcher {
48
58
  timeout: number;
49
59
  active: boolean;
50
60
  timer: undefined | ReturnType<typeof setTimeout>;
61
+ launchCounter: number;
51
62
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-embedded-ui",
3
- "version": "1.1.3",
3
+ "version": "1.2.2",
4
4
  "files": [
5
5
  "dist"
6
6
  ],
@@ -1,48 +0,0 @@
1
- import {createRequestActionTypes, createApiRequest} from '../utils';
2
- import '../../services/api';
3
-
4
- const FETCH_CLUSTER = createRequestActionTypes('cluster', 'FETCH_CLUSTER');
5
-
6
- const initialState = {loading: false};
7
-
8
- const clusterInfo = function (state = initialState, action) {
9
- switch (action.type) {
10
- case FETCH_CLUSTER.REQUEST: {
11
- return {
12
- ...state,
13
- loading: true,
14
- };
15
- }
16
- case FETCH_CLUSTER.SUCCESS: {
17
- const {data = {}} = action;
18
-
19
- return {
20
- ...state,
21
- ...data,
22
- loading: false,
23
- error: undefined,
24
- };
25
- }
26
- case FETCH_CLUSTER.FAILURE: {
27
- return {
28
- ...state,
29
- error: action.error,
30
- loading: false,
31
- };
32
- }
33
- default:
34
- return state;
35
- }
36
- };
37
-
38
- export function getCluster(name) {
39
- return createApiRequest({
40
- request: window.api.getClustersList(),
41
- actions: FETCH_CLUSTER,
42
- dataHandler: ({clusters = []}) => {
43
- return clusters.filter((item) => item.name === name)[0];
44
- },
45
- });
46
- }
47
-
48
- export default clusterInfo;