@orsetra/shared-ui 1.1.18 → 1.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orsetra/shared-ui",
3
- "version": "1.1.18",
3
+ "version": "1.1.20",
4
4
  "description": "Shared UI components for Orsetra platform",
5
5
  "main": "./index.ts",
6
6
  "types": "./index.ts",
@@ -16,6 +16,12 @@
16
16
  "components",
17
17
  "hooks",
18
18
  "lib",
19
+ "api",
20
+ "context",
21
+ "utils",
22
+ "types",
23
+ "locals",
24
+ "i18n.tsx",
19
25
  "README.md"
20
26
  ],
21
27
  "publishConfig": {
@@ -43,11 +49,6 @@
43
49
  "@hookform/resolvers": "^3.9.1",
44
50
  "@orsetra/shared-types": "^1.0.4",
45
51
  "@radix-ui/react-accordion": "1.2.2",
46
- "i18next": "^23.15.0",
47
- "lodash": "^4.17.21",
48
- "moment": "^2.30.1",
49
- "react-cookies": "^0.1.1",
50
- "react-i18next": "^14.1.0",
51
52
  "@radix-ui/react-alert-dialog": "1.1.4",
52
53
  "@radix-ui/react-aspect-ratio": "1.1.1",
53
54
  "@radix-ui/react-avatar": "1.1.2",
@@ -79,13 +80,18 @@
79
80
  "cmdk": "1.0.4",
80
81
  "date-fns": "4.1.0",
81
82
  "embla-carousel-react": "8.5.1",
83
+ "i18next": "^23.15.0",
82
84
  "input-otp": "1.4.1",
85
+ "lodash": "^4.17.21",
83
86
  "lucide-react": "^0.454.0",
87
+ "moment": "^2.30.1",
84
88
  "next-themes": "^0.4.4",
85
89
  "react-avatar": "^5.0.3",
90
+ "react-cookies": "^0.1.1",
86
91
  "react-day-picker": "8.10.1",
87
92
  "react-easy-crop": "^5.0.8",
88
93
  "react-hook-form": "^7.54.0",
94
+ "react-i18next": "^14.1.0",
89
95
  "react-resizable-panels": "^2.1.7",
90
96
  "recharts": "^2.15.0",
91
97
  "sonner": "^1.7.1",
@@ -94,6 +100,7 @@
94
100
  "zod": "^3.24.1"
95
101
  },
96
102
  "devDependencies": {
103
+ "@types/lodash": "^4.17.24",
97
104
  "@types/react": "^19",
98
105
  "next": "^16.0.7",
99
106
  "typescript": "^5"
@@ -0,0 +1,6 @@
1
+ export enum DeployModes {
2
+ Deploy = 'deploy',
3
+ CanaryDeploy = 'canary-deploy',
4
+ }
5
+
6
+ export type DeployMode = DeployModes.CanaryDeploy | DeployModes.Deploy;
package/types/data.ts ADDED
@@ -0,0 +1,9 @@
1
+ export type KeyValue<T = any> = Record<string, T>;
2
+
3
+ export function unmarshal<T>(kv: KeyValue): T {
4
+ return kv as T;
5
+ }
6
+
7
+ export function marshal<T>(obj: T): KeyValue {
8
+ return obj as KeyValue;
9
+ }
package/types/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './layout';
2
+ export * from './menus';
3
+ export * from './permission';
4
+ export * from './data';
5
+ export * from './application';
@@ -0,0 +1,7 @@
1
+ export type LayoutMode = LayoutModes.Default | LayoutModes.Neat | LayoutModes.NeatPro;
2
+
3
+ export enum LayoutModes {
4
+ Default = 'default',
5
+ Neat = 'neat',
6
+ NeatPro = 'neat2',
7
+ }
package/types/menus.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { ResourceAction } from './permission';
2
+
3
+ export interface Workspace {
4
+ name: string;
5
+ icon?: string | React.ReactNode;
6
+ label?: string;
7
+ rootRoute: string;
8
+ }
9
+
10
+ type Route = RegExp | string;
11
+
12
+ export enum MenuTypes {
13
+ Workspace = 'Workspace',
14
+ ApplicationEnv = 'ApplicationEnv',
15
+ Project = 'Project',
16
+ }
17
+
18
+ export type MenuType = MenuTypes.Workspace | MenuTypes.ApplicationEnv | MenuTypes.Project;
19
+
20
+ export interface Menu {
21
+ workspace: string;
22
+ type: MenuType;
23
+ catalog?: string;
24
+ name: string;
25
+ label: string;
26
+ to: string;
27
+ href?: string;
28
+ relatedRoute: Route[];
29
+ icon?: string | React.ReactNode;
30
+ permission?: ResourceAction;
31
+ active?: boolean;
32
+ }
@@ -0,0 +1,4 @@
1
+ export interface ResourceAction {
2
+ resource: string;
3
+ action: string;
4
+ };
@@ -0,0 +1,26 @@
1
+ import 'mocha';
2
+ import { assert } from 'chai';
3
+
4
+ import { equalArray } from '../common';
5
+ import { resourceMatch, ResourceName } from '../permission';
6
+
7
+ describe('test permission', () => {
8
+ it('test resourceMatch', () => {
9
+ assert.equal(resourceMatch(new ResourceName('project:*'), new ResourceName('project:*')), true);
10
+ assert.equal(
11
+ resourceMatch(
12
+ new ResourceName('projects:abc/application:bcd'),
13
+ new ResourceName('projects:*/application:*'),
14
+ ),
15
+ true,
16
+ );
17
+ });
18
+ });
19
+
20
+ describe('test util', () => {
21
+ it('test equal the array', () => {
22
+ assert.equal(equalArray(['a', 'b'], ['b', 'a']), true);
23
+ assert.equal(equalArray(['b'], ['b', 'a']), false);
24
+ assert.equal(equalArray(undefined, ['b', 'a']), false);
25
+ });
26
+ });
package/utils/cache.ts ADDED
@@ -0,0 +1,9 @@
1
+ import cookie from 'react-cookies';
2
+
3
+ export function setData(key: string, value: string, expire?: Date) {
4
+ cookie.save(key, value, { expires: expire });
5
+ }
6
+
7
+ export function getData(key: string) {
8
+ return cookie.load(key);
9
+ }
@@ -0,0 +1,314 @@
1
+ import moment from 'moment';
2
+
3
+ import type { AddonBaseStatus } from '@/api';
4
+
5
+ type Navigator = {
6
+ language: string;
7
+ };
8
+
9
+ export function getLanguage() {
10
+ const navigator: Navigator = window.navigator;
11
+ const lang = navigator.language || 'en';
12
+ return localStorage.getItem('lang') || lang.split('-')[0];
13
+ }
14
+
15
+ export function isMock() {
16
+ return process.env.MOCK ? true : false;
17
+ }
18
+
19
+ export function getDomain(): { MOCK: string | undefined; APIBASE: string | undefined } {
20
+ const { MOCK, BASE_DOMAIN } = process.env;
21
+ return {
22
+ MOCK: MOCK || '',
23
+ APIBASE: BASE_DOMAIN || '',
24
+ };
25
+ }
26
+
27
+ export function isApplicationPath(pathname: string) {
28
+ return (pathname || '').startsWith('/applications');
29
+ }
30
+ export function isClustersPath(pathname: string) {
31
+ return (pathname || '').startsWith('/clusters');
32
+ }
33
+ export function isAddonsPath(pathname: string) {
34
+ return (pathname || '').startsWith('/addons');
35
+ }
36
+ export function isOperationPath(pathname: string) {
37
+ return (pathname || '').startsWith('/operation');
38
+ }
39
+ export function isModelPath(pathname: string) {
40
+ return (pathname || '').startsWith('/model');
41
+ }
42
+
43
+ export function isAPPStorePath(pathname: string) {
44
+ return (pathname || '').startsWith('/appstores');
45
+ }
46
+
47
+ export function isTargetURL(pathname: string) {
48
+ return (pathname || '').startsWith('/targets');
49
+ }
50
+
51
+ export function isEnvPath(pathname: string) {
52
+ return (pathname || '').startsWith('/envs');
53
+ }
54
+
55
+ export function isPipelinePath(pathname: string) {
56
+ if ((pathname || '').startsWith('/pipelines')) {
57
+ return true;
58
+ }
59
+ const re = new RegExp('^/projects/.*./pipelines.*');
60
+ return re.test(pathname);
61
+ }
62
+
63
+ export function isUsersPath(pathname: string) {
64
+ return (pathname || '').startsWith('/users');
65
+ }
66
+ export function isProjectPath(pathname: string) {
67
+ const re = new RegExp('^/projects/.*./pipelines.*');
68
+ if (re.test(pathname)) {
69
+ return false;
70
+ }
71
+ return (pathname || '').startsWith('/projects');
72
+ }
73
+
74
+ export function isRolesPath(pathname: string) {
75
+ return (pathname || '').startsWith('/roles');
76
+ }
77
+
78
+ export function isConfigPath(pathname: string) {
79
+ return (pathname || '').startsWith('/config');
80
+ }
81
+
82
+ export function isDefinitionsPath(pathname: string) {
83
+ return (pathname || '').startsWith('/definitions');
84
+ }
85
+
86
+ export const APPLICATION_PATH = 'applications';
87
+ export const CLUSTERS_PATH = 'clusters';
88
+ export const ADDONS_PATH = 'addons';
89
+ export const WORKFLOWS_PATH = 'workflows';
90
+
91
+ export function momentDate(time: undefined | string): string {
92
+ if (!time) {
93
+ return '';
94
+ }
95
+ return moment(time).format('YYYY/MM/DD HH:mm:ss');
96
+ }
97
+
98
+ export function momentShortDate(time: undefined | string): string {
99
+ if (!time) {
100
+ return '';
101
+ }
102
+ return moment(time).format('YYYY/MM/DD');
103
+ }
104
+
105
+ export function beautifyTime(time?: string) {
106
+ if (!time || time === '0001-01-01T00:00:00Z') {
107
+ return '';
108
+ }
109
+ const timestamp = moment(time).unix();
110
+ const now = new Date();
111
+ let mistiming = Math.round(now.getTime() / 1000) - timestamp;
112
+ const postfix = mistiming > 0 ? 'ago' : 'later';
113
+ mistiming = Math.abs(mistiming);
114
+ const arrr = ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds'];
115
+ const arrn = [31536000, 2592000, 604800, 86400, 3600, 60, 1];
116
+
117
+ for (let i = 0; i < 7; i++) {
118
+ const inm = Math.floor(mistiming / arrn[i]);
119
+ if (inm != 0) {
120
+ return inm + ' ' + arrr[i] + ' ' + postfix;
121
+ }
122
+ }
123
+ return '';
124
+ }
125
+
126
+ export function timeDiff(start?: string, end?: string): string {
127
+ if (!start || start == '0001-01-01T00:00:00Z') {
128
+ return '-';
129
+ }
130
+ let endTime = moment(moment.now());
131
+ if (end && end != '0001-01-01T00:00:00Z') {
132
+ endTime = moment(end);
133
+ }
134
+ const seconds = endTime.diff(moment(start), 'seconds');
135
+ if (seconds > 60) {
136
+ return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
137
+ }
138
+ return `${seconds}s`;
139
+ }
140
+
141
+ export function beautifyBinarySize(value?: number) {
142
+ if (null == value || value == 0) {
143
+ return '0 Bytes';
144
+ }
145
+ const unitArr = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
146
+ let index = 0;
147
+ index = Math.floor(Math.log(value) / Math.log(1024));
148
+ const size = value / Math.pow(1024, index);
149
+ let sizeStr = '';
150
+ if (size % 1 === 0) {
151
+ sizeStr = size.toFixed(0);
152
+ } else {
153
+ sizeStr = size.toFixed(2);
154
+ }
155
+ return sizeStr + unitArr[index];
156
+ }
157
+
158
+ export const checkName = /^[a-z]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/;
159
+ export const urlRegular = /(https|http|ftp|file):\/\/[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]/g;
160
+ export const checkImageName = /^[^\u4e00-\u9fa5 ]{0,512}$/;
161
+
162
+ export const formItemLayout = {
163
+ labelCol: {
164
+ fixedSpan: 6,
165
+ },
166
+ wrapperCol: {
167
+ span: 18,
168
+ },
169
+ };
170
+
171
+ export const ACKClusterStatus = [
172
+ {
173
+ key: 'initial',
174
+ value: '集群创建中',
175
+ color: '#98af88',
176
+ },
177
+ {
178
+ key: 'failed',
179
+ value: '集群创建失败',
180
+ color: '#ef1111',
181
+ },
182
+ {
183
+ key: 'running',
184
+ value: '集群运行中',
185
+ color: '#10e60e',
186
+ },
187
+ {
188
+ key: 'updating',
189
+ value: '集群升级中',
190
+ color: '#10e60e',
191
+ },
192
+ {
193
+ key: 'updating_failed',
194
+ value: '集群升级失败',
195
+ color: '#ef1111',
196
+ },
197
+ {
198
+ key: 'scaling',
199
+ value: '集群伸缩中',
200
+ color: '#10e60e',
201
+ },
202
+ {
203
+ key: 'stopped',
204
+ value: '集群已经停止运行',
205
+ color: '#3a3e3a',
206
+ },
207
+ {
208
+ key: 'deleting',
209
+ value: '集群删除中',
210
+ color: '#fd940f',
211
+ },
212
+ {
213
+ key: 'deleted',
214
+ value: '集群已经被删除',
215
+ color: '#3a3e3a',
216
+ },
217
+ {
218
+ key: 'delete_failed',
219
+ value: '集群删除失败',
220
+ color: '#ef1111',
221
+ },
222
+ ];
223
+
224
+ export const replaceUrl = function (text: string) {
225
+ const re = /(http[s]?:\/\/([\w-]+.)+([:\d+])?(\/[\w-\.\/\?%&=]*)?)/gi;
226
+ const str = text.replace(re, function (a) {
227
+ return '<a href="' + a + '" target=_blank>' + a + '</a>';
228
+ });
229
+ return str;
230
+ };
231
+
232
+ export const checkUserPassword = /^(?=.*[0-9])(?=.*[a-zA-Z])([a-zA-Z0-9]{8,16})$/;
233
+
234
+ export function isMatchBusinessCode(businessCode: number) {
235
+ const tokenExpiredList = [12002, 12010];
236
+ return tokenExpiredList.includes(businessCode);
237
+ }
238
+
239
+ export function equalArray(a?: string[], b?: string[]) {
240
+ if (a === undefined && b === undefined) {
241
+ return true;
242
+ }
243
+ if (b === undefined || a === undefined) {
244
+ return false;
245
+ }
246
+ if (a.length !== b.length) {
247
+ return false;
248
+ } else {
249
+ const sa = a.sort();
250
+ const sb = b.sort();
251
+ for (let i = 0; i < sa.length; i++) {
252
+ if (sa[i] !== sb[i]) {
253
+ return false;
254
+ }
255
+ }
256
+ return true;
257
+ }
258
+ }
259
+
260
+ export function intersectionArray(a?: string[], b?: string[]): string[] | undefined {
261
+ if (a == undefined || b == undefined) {
262
+ return undefined;
263
+ }
264
+ return a.filter((v) => b.indexOf(v) > -1);
265
+ }
266
+
267
+ export function checkEnabledAddon(addonName: string, enabledAddons?: AddonBaseStatus[]) {
268
+ if (!enabledAddons) {
269
+ return false;
270
+ }
271
+ const addonNames = enabledAddons.map((addon) => {
272
+ return addon.name;
273
+ });
274
+ if (addonNames.includes(addonName)) {
275
+ return true;
276
+ }
277
+ return false;
278
+ }
279
+
280
+ export function convertAny(data?: any): string {
281
+ if (!data) {
282
+ return '';
283
+ }
284
+ switch (typeof data) {
285
+ case 'string':
286
+ return data;
287
+ case 'boolean':
288
+ return data === true ? 'true' : 'false';
289
+ case 'object':
290
+ return JSON.stringify(data);
291
+ case 'number':
292
+ return data.toString();
293
+ default:
294
+ return '';
295
+ }
296
+ }
297
+
298
+ export function showAlias(name: string, alias?: string): string;
299
+ export function showAlias(item: { name: string; alias?: string }): string;
300
+ export function showAlias(item: { name: string; alias?: string } | string, alias?: string) {
301
+ if (typeof item === 'string') {
302
+ if (alias) {
303
+ return `${item}(${alias})`;
304
+ }
305
+ return item;
306
+ }
307
+ if (!item) {
308
+ return '';
309
+ }
310
+ if (item.alias) {
311
+ return `${item.name}(${item.alias})`;
312
+ }
313
+ return item.name;
314
+ }
@@ -0,0 +1,31 @@
1
+ /*
2
+ Copyright 2021 The KubeVela Authors.
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { isMatchBusinessCode } from './common';
18
+
19
+ export interface APIError {
20
+ BusinessCode: number;
21
+ Message: string;
22
+ }
23
+
24
+ export function handleError(err: APIError) {
25
+ // TODO: Handle errors based on businessCode
26
+ const isShowErrFlag = isMatchBusinessCode(err.BusinessCode);
27
+ if (err && !isShowErrFlag) {
28
+ // TODO: Replace with proper toast notification
29
+ console.error(err.Message);
30
+ }
31
+ }
package/utils/i18n.tsx ADDED
@@ -0,0 +1,28 @@
1
+ import i18n from 'i18next';
2
+ import { initReactI18next } from 'react-i18next';
3
+
4
+ import En from './locals/En/en.json';
5
+ import Zh from './locals/Zh/zh.json';
6
+ import { getLanguage } from './utils/common';
7
+
8
+ const resources = {
9
+ en: {
10
+ translation: En,
11
+ },
12
+ zh: {
13
+ translation: Zh,
14
+ },
15
+ };
16
+
17
+ const currentLanguage = getLanguage();
18
+ i18n
19
+ .use(initReactI18next) // passes i18n down to react-i18next
20
+ .init({
21
+ resources,
22
+ lng: currentLanguage,
23
+ keySeparator: false, // we do not use keys in form messages.welcome
24
+ interpolation: {
25
+ escapeValue: false, // react already safes from xss
26
+ },
27
+ });
28
+ export default i18n;
package/utils/icon.tsx ADDED
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+
3
+ const nameUpper = (name: string) => {
4
+ return name
5
+ .split('-')
6
+ .map((sep) => {
7
+ if (sep.length > 0) {
8
+ return sep.toUpperCase()[0];
9
+ }
10
+ return sep;
11
+ })
12
+ .toString()
13
+ .replace(',', '');
14
+ };
15
+
16
+ export const renderIcon = (name: string, icon?: string, size?: string) => {
17
+ console.log(icon)
18
+ if (icon!=="" && icon!=="null" && icon!==undefined) {
19
+ if (!icon.startsWith("/")){
20
+ icon = "/"+icon
21
+ }
22
+ return <img style={{width: size??"60px", height: size??"60px" }} src={icon} />;
23
+ } else {
24
+ return (
25
+ <div
26
+ style={{
27
+ display: 'inline-block',
28
+ verticalAlign: 'middle',
29
+ padding: `2px 4px`,
30
+ width: '60px',
31
+ height: '60px',
32
+ borderRadius: '50%',
33
+ backgroundColor: '#fff',
34
+ textAlign: 'center',
35
+ lineHeight: '60px',
36
+ }}
37
+ >
38
+ <span style={{ color: '#1b58f4', fontSize: `2em` }}>{nameUpper(name)}</span>
39
+ </div>
40
+ );
41
+ }
42
+ }