Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit cdda95b

Browse files
authoredMay 1, 2024
Merge pull request #843 from raheeliftikhar5/user-api-keys
User API Keys
2 parents 345d295 + 040e400 commit cdda95b

File tree

14 files changed

+461
-13
lines changed

14 files changed

+461
-13
lines changed
 

‎client/packages/lowcoder/src/api/userApi.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,20 @@ export interface GetUserResponse extends ApiResponse {
3838
} & BaseUserInfo;
3939
}
4040

41+
export interface ApiKeyPayload {
42+
name: string;
43+
description?: string;
44+
}
45+
46+
export interface FetchApiKeysResponse extends ApiResponse {
47+
data: {
48+
id: string;
49+
name: string;
50+
description: string;
51+
token: string;
52+
}
53+
}
54+
4155
export type GetCurrentUserResponse = GenericApiResponse<CurrentUser>;
4256

4357
class UserApi extends Api {
@@ -55,6 +69,9 @@ class UserApi extends Api {
5569
static markUserStatusURL = "/users/mark-status";
5670
static userDetailURL = (id: string) => `/users/userDetail/${id}`;
5771
static resetPasswordURL = `/users/reset-password`;
72+
static fetchApiKeysURL = `/auth/api-keys`;
73+
static createApiKeyURL = `/auth/api-key`;
74+
static deleteApiKeyURL = (id: string) => `/auth/api-key/${id}`;
5875

5976
static thirdPartyLogin(
6077
request: ThirdPartyAuthRequest & CommonLoginParam
@@ -120,6 +137,24 @@ class UserApi extends Api {
120137
static resetPassword(userId: string): AxiosPromise<ApiResponse> {
121138
return Api.post(UserApi.resetPasswordURL, { userId: userId });
122139
}
140+
141+
static createApiKey({
142+
name,
143+
description = ''
144+
}: ApiKeyPayload): AxiosPromise<ApiResponse> {
145+
return Api.post(UserApi.createApiKeyURL, {
146+
name,
147+
description
148+
});
149+
}
150+
151+
static fetchApiKeys(): AxiosPromise<ApiResponse> {
152+
return Api.get(UserApi.fetchApiKeysURL);
153+
}
154+
155+
static deleteApiKey(apiKeyId: string): AxiosPromise<ApiResponse> {
156+
return Api.delete(UserApi.deleteApiKeyURL(apiKeyId));
157+
}
123158
}
124159

125160
export default UserApi;

‎client/packages/lowcoder/src/constants/reduxActionConstants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ export const ReduxActionTypes = {
77
FETCH_CURRENT_USER_SUCCESS: "FETCH_CURRENT_USER_SUCCESS",
88
FETCH_RAW_CURRENT_USER: "FETCH_RAW_CURRENT_USER",
99
FETCH_RAW_CURRENT_USER_SUCCESS: "FETCH_RAW_CURRENT_USER_SUCCESS",
10+
FETCH_API_KEYS: "FETCH_API_KEYS",
11+
FETCH_API_KEYS_SUCCESS: "FETCH_API_KEYS_SUCCESS",
12+
1013

1114
/* plugin RELATED */
1215
FETCH_DATA_SOURCE_TYPES: "FETCH_DATA_SOURCE_TYPES",

‎client/packages/lowcoder/src/constants/userConstants.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,11 @@ export const defaultCurrentUser: CurrentUser = {
8383
extra: {},
8484
};
8585

86+
export type ApiKey = {
87+
id: string;
88+
name: string;
89+
description: string;
90+
token: string;
91+
}
92+
8693
export type UserStatusType = keyof BaseUserInfo["userStatus"];

‎client/packages/lowcoder/src/i18n/locales/de.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2261,7 +2261,15 @@ export const de: typeof en = {
22612261
"createdApps": "Erstellte Apps",
22622262
"createdModules": "Erstellte Module",
22632263
"onMarketplace": "Auf dem Marktplatz",
2264-
"howToPublish": "So veröffentlichen Sie auf dem Marktplatz"
2264+
"howToPublish": "So veröffentlichen Sie auf dem Marktplatz",
2265+
"apiKeys": "API-Schlüssel",
2266+
"createApiKey": "API-Schlüssel erstellen",
2267+
"apiKeyName": "Name",
2268+
"apiKeyDescription": "Beschreibung",
2269+
"apiKey": "API-Schlüssel",
2270+
"deleteApiKey": "API-Schlüssel löschen",
2271+
"deleteApiKeyContent": "Sind Sie sicher, dass Sie diesen API-Schlüssel löschen möchten?",
2272+
"deleteApiKeyError": "Etwas ist schief gelaufen. Bitte versuche es erneut."
22652273
},
22662274
"shortcut": {
22672275
...en.shortcut,

‎client/packages/lowcoder/src/i18n/locales/en.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2314,7 +2314,15 @@ export const en = {
23142314
"createdApps": "Created Apps",
23152315
"createdModules": "Created Modules",
23162316
"onMarketplace": "On Marketplace",
2317-
"howToPublish": "How to publish on Marketplace"
2317+
"howToPublish": "How to publish on Marketplace",
2318+
"apiKeys": "API Keys",
2319+
"createApiKey": "Create API Key",
2320+
"apiKeyName": "Name",
2321+
"apiKeyDescription": "Description",
2322+
"apiKey": "API Key",
2323+
"deleteApiKey": "Delete API Key",
2324+
"deleteApiKeyContent": "Are you sure you want to delete this API key?",
2325+
"deleteApiKeyError": "Something went wrong. Please try again."
23182326
},
23192327
"shortcut": {
23202328
"shortcutList": "Keyboard Shortcuts",

‎client/packages/lowcoder/src/i18n/locales/zh.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2148,6 +2148,14 @@ profile: {
21482148
createdModules: "创建的模块",
21492149
onMarketplace: "在市场上",
21502150
howToPublish: "如何在 Marketplace 上发布",
2151+
apiKeys: "API 密钥",
2152+
createApiKey: "创建 API 密钥",
2153+
apiKeyName: "姓名",
2154+
apiKeyDescription: "描述",
2155+
apiKey: "API密钥",
2156+
deleteApiKey: "删除 API 密钥",
2157+
deleteApiKeyContent: "您确定要删除此 API 密钥吗?",
2158+
deleteApiKeyError: "出了些问题。请再试一次。"
21512159
},
21522160
shortcut: {
21532161
shortcutList: "键盘快捷键",

‎client/packages/lowcoder/src/pages/ApplicationV2/UserProfileLayout.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import React from 'react';
21
import CountUp from 'react-countup';
32
import { useSelector } from "react-redux";
43
import styled from "styled-components";
@@ -20,7 +19,7 @@ import { ALL_APPLICATIONS_URL } from "constants/routesURL";
2019
import { USER_PROFILE_URL } from "constants/routesURL";
2120
import { default as Divider } from "antd/es/divider";
2221

23-
import { Avatar, Badge, Button, Card, Col, Row, Space, Typography, Select } from 'antd';
22+
import { Avatar, Badge, Button, Card, Col, Row, Space, Typography, Select, Table, Flex } from "antd";
2423

2524
import {
2625
BlurFinishInput,
@@ -40,6 +39,7 @@ import { updateUserAction, updateUserSuccess } from "redux/reduxActions/userActi
4039
import { default as Upload, UploadChangeParam } from "antd/es/upload";
4140
import { USER_HEAD_UPLOAD_URL } from "constants/apiConstants";
4241
import { messageInstance } from "lowcoder-design/src/components/GlobalInstances";
42+
import UserApiKeysCard from './components/UserApiKeysCard';
4343

4444
const { Text, Title, Link } = Typography;
4545
const { Option } = Select;
@@ -161,7 +161,9 @@ export function UserProfileLayout(props: UserProfileLayoutProps) {
161161
const modules = useSelector(modulesSelector);
162162
const orgUsers = useSelector(getOrgUsers);
163163
const orgGroups = useSelector(getOrgGroups);
164+
164165
const currentOrgId = user.currentOrgId;
166+
165167
const currentOrg = useMemo(
166168
() => user.orgs.find((o) => o.id === currentOrgId),
167169
[user, currentOrgId]
@@ -192,9 +194,6 @@ export function UserProfileLayout(props: UserProfileLayoutProps) {
192194
}, 1000);
193195
};
194196

195-
console.log("App Language", language);
196-
console.log("User Language", currentUser.uiLanguage);
197-
198197
if (!user.currentOrgId) {
199198
return null;
200199
}
@@ -395,7 +394,9 @@ export function UserProfileLayout(props: UserProfileLayoutProps) {
395394

396395
</Space>
397396
</Card>
398-
397+
398+
<UserApiKeysCard />
399+
399400
</ProfileView>
400401
</ContentWrapper>
401402
</Wrapper>
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
1-
import { useSelector } from "react-redux";
1+
import { useDispatch, useSelector } from "react-redux";
22
import { UserProfileLayout } from "./UserProfileLayout";
33
import { getUser } from "../../redux/selectors/usersSelectors";
44
import { trans } from "../../i18n";
55
import { USER_PROFILE_URL } from "constants/routesURL";
6+
import { useEffect } from "react";
7+
import { fetchApiKeysAction } from "redux/reduxActions/userActions";
68

79
export function UserProfileView() {
810

911
const user = useSelector(getUser);
12+
const dispatch = useDispatch();
13+
14+
useEffect(() => {
15+
if (!user.currentOrgId) return;
16+
17+
dispatch(fetchApiKeysAction());
18+
}, []);
1019

1120
if (!user.currentOrgId) {
1221
return null;
1322
}
1423

24+
1525
return <UserProfileLayout breadcrumb={[{ text: trans("home.profile"), path: USER_PROFILE_URL }]}/>;
1626

1727
};
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { useEffect, useMemo, useState } from "react";
2+
import {
3+
messageInstance,
4+
CustomSelect,
5+
CloseEyeIcon,
6+
CustomModal,
7+
UnderlineCss,
8+
} from "lowcoder-design";
9+
import { trans } from "i18n";
10+
import { default as Form } from "antd/es/form";
11+
import { default as Input } from "antd/es/input";
12+
import { validateResponse } from "api/apiUtils";
13+
import _ from "lodash";
14+
import { styled } from "styled-components";
15+
import UserApi, { ApiKeyPayload } from "api/userApi";
16+
17+
const CustomModalStyled = styled(CustomModal)`
18+
button {
19+
margin-top: 20px;
20+
}
21+
`;
22+
23+
const FormStyled = styled(Form)`
24+
.ant-form-item-control-input-content > input,
25+
.ant-input-password {
26+
&:hover {
27+
border-color: #8b8fa3;
28+
}
29+
30+
&:focus,
31+
&.ant-input-affix-wrapper-focused {
32+
border-color: #3377ff;
33+
}
34+
}
35+
36+
.ant-form-item-label > label {
37+
font-size: 13px;
38+
line-height: 19px;
39+
.has-tip {
40+
${UnderlineCss};
41+
}
42+
}
43+
44+
.ant-input-password-icon.anticon {
45+
color: #8b8fa3;
46+
47+
&:hover {
48+
color: #222;
49+
}
50+
}
51+
52+
&.ant-form-vertical .ant-form-item-label {
53+
padding-bottom: 4px;
54+
}
55+
56+
.ant-form-item-explain-error {
57+
font-size: 12px;
58+
color: #f73131;
59+
line-height: 20px;
60+
}
61+
62+
.ant-form-item-label
63+
> label.ant-form-item-required:not(.ant-form-item-required-mark-optional)::before {
64+
color: #f73131;
65+
}
66+
67+
.ant-input-status-error:not(.ant-input-disabled):not(.ant-input-borderless).ant-input,
68+
.ant-input-status-error:not(.ant-input-disabled):not(.ant-input-borderless).ant-input:hover {
69+
border-color: #f73131;
70+
}
71+
72+
.register {
73+
margin: -4px 0 20px 0;
74+
}
75+
76+
.ant-input-prefix {
77+
margin-right: 8px;
78+
svg {
79+
path,
80+
rect:nth-of-type(1) {
81+
stroke: #8b8fa3;
82+
}
83+
rect:nth-of-type(2) {
84+
fill: #8b8fa3;
85+
}
86+
}
87+
}
88+
`;
89+
90+
type CreateApiKeyModalProps = {
91+
modalVisible: boolean;
92+
closeModal: () => void;
93+
onConfigCreate: () => void;
94+
};
95+
96+
function CreateApiKeyModal(props: CreateApiKeyModalProps) {
97+
const {
98+
modalVisible,
99+
closeModal,
100+
onConfigCreate
101+
} = props;
102+
const [form] = Form.useForm();
103+
const [saveLoading, setSaveLoading] = useState(false);
104+
105+
const handleOk = () => {
106+
form.validateFields().then(values => {
107+
// console.log(values)
108+
createApiKey(values)
109+
})
110+
}
111+
function createApiKey(values: ApiKeyPayload) {
112+
setSaveLoading(true);
113+
114+
UserApi.createApiKey(values)
115+
.then((resp) => {
116+
if (validateResponse(resp)) {
117+
messageInstance.success(trans("idSource.saveSuccess"));
118+
}
119+
})
120+
.catch((e) => messageInstance.error(e.message))
121+
.finally(() => {
122+
setSaveLoading(false);
123+
onConfigCreate();
124+
});
125+
}
126+
127+
function handleCancel() {
128+
closeModal();
129+
form.resetFields();
130+
}
131+
132+
return (
133+
<CustomModalStyled
134+
width="500px"
135+
title={"Create API Key"}
136+
open={modalVisible}
137+
okText={"Save"}
138+
okButtonProps={{
139+
loading: saveLoading
140+
}}
141+
onOk={handleOk}
142+
onCancel={handleCancel}
143+
destroyOnClose
144+
afterClose={() => form.resetFields()}
145+
>
146+
<FormStyled
147+
form={form}
148+
name="basic"
149+
layout="vertical"
150+
style={{ maxWidth: 440 }}
151+
autoComplete="off"
152+
>
153+
<Form.Item
154+
name="name"
155+
label="Name"
156+
rules={[{ required: true }]}
157+
>
158+
<Input
159+
placeholder={trans("idSource.formPlaceholder", {
160+
label: 'Name'
161+
})}
162+
/>
163+
</Form.Item>
164+
<Form.Item
165+
name="description"
166+
label="Description"
167+
>
168+
<Input
169+
placeholder={trans("idSource.formPlaceholder", {
170+
label: 'Description'
171+
})}
172+
/>
173+
</Form.Item>
174+
</FormStyled>
175+
</CustomModalStyled>
176+
);
177+
}
178+
179+
export default CreateApiKeyModal;
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { getApiKeys } from "redux/selectors/usersSelectors";
2+
import Card from "antd/es/card";
3+
import Flex from "antd/es/flex";
4+
import Title from "antd/es/typography/Title";
5+
import Table from "antd/es/table";
6+
import { useDispatch, useSelector } from "react-redux";
7+
import { useState } from "react";
8+
import { styled } from "styled-components";
9+
import { AddIcon, CustomModal, EditPopover, TacoButton, messageInstance } from "lowcoder-design";
10+
import { trans } from "i18n";
11+
import { PopoverIcon } from "pages/setting/permission/styledComponents";
12+
import CreateApiKeyModal from "./CreateApiKeyModal";
13+
import { fetchApiKeysAction } from "redux/reduxActions/userActions";
14+
import UserApi from "@lowcoder-ee/api/userApi";
15+
import { validateResponse } from "@lowcoder-ee/api/apiUtils";
16+
17+
const TableStyled = styled(Table)`
18+
.ant-table-tbody > tr > td {
19+
padding: 11px 12px;
20+
}
21+
`;
22+
23+
const OperationWrapper = styled.div`
24+
display: flex;
25+
align-items: center;
26+
justify-content: flex-end;
27+
`;
28+
29+
const CreateButton = styled(TacoButton)`
30+
svg {
31+
margin-right: 2px;
32+
width: 12px;
33+
height: 12px;
34+
}
35+
36+
box-shadow: none;
37+
`;
38+
39+
export default function UserApiKeysCard() {
40+
const dispatch = useDispatch();
41+
const apiKeys = useSelector(getApiKeys);
42+
const [modalVisible, setModalVisible] = useState(false);
43+
44+
return (
45+
<>
46+
<Card style={{ marginBottom: "20px" }}>
47+
<Flex justify="space-between" align="center" style={{marginBottom: '8px'}}>
48+
<Title level={4}>{trans("profile.apiKeys")}</Title>
49+
<CreateButton
50+
buttonType={"primary"}
51+
icon={<AddIcon />}
52+
onClick={() =>
53+
setModalVisible(true)
54+
}
55+
>
56+
{trans("profile.createApiKey")}
57+
</CreateButton>
58+
</Flex>
59+
<TableStyled
60+
tableLayout={"auto"}
61+
scroll={{ x: "100%" }}
62+
pagination={false}
63+
onRow={(record) => ({
64+
65+
})}
66+
columns={[
67+
{
68+
title: trans("profile.apiKeyName"),
69+
dataIndex: "name",
70+
ellipsis: true,
71+
},
72+
{
73+
title: trans("profile.apiKeyDescription"),
74+
dataIndex: "description",
75+
ellipsis: true,
76+
render: (value: string) => {
77+
return (
78+
<>
79+
{ value || '-'}
80+
</>
81+
)
82+
}
83+
},
84+
{
85+
title: trans("profile.apiKey"),
86+
dataIndex: "token",
87+
ellipsis: true,
88+
render: (value: string) => {
89+
const startToken = value.substring(0, 6);
90+
const endToken = value.substring(value.length - 6);
91+
return (
92+
<>
93+
{ `${startToken}********************${endToken}`}
94+
</>
95+
)
96+
}
97+
},
98+
{ title: " ", dataIndex: "operation", width: "208px" },
99+
]}
100+
dataSource={apiKeys.map((apiKey, i) => ({
101+
...apiKey,
102+
key: i,
103+
operation: (
104+
<OperationWrapper>
105+
<EditPopover
106+
del={() => {
107+
CustomModal.confirm({
108+
title: trans("profile.deleteApiKey"),
109+
content: trans("profile.deleteApiKeyContent"),
110+
onConfirm: () => {
111+
UserApi.deleteApiKey(apiKey.id).then(resp => {
112+
if(validateResponse(resp)) {
113+
dispatch(fetchApiKeysAction());
114+
}
115+
})
116+
.catch((e) => {
117+
messageInstance.error(trans("profile.deleteApiKeyError"));
118+
})
119+
},
120+
confirmBtnType: "delete",
121+
okText: trans("delete"),
122+
})
123+
}}
124+
>
125+
<PopoverIcon tabIndex={-1} />
126+
</EditPopover>
127+
</OperationWrapper>
128+
),
129+
}))}
130+
/>
131+
</Card>
132+
133+
<CreateApiKeyModal
134+
modalVisible={modalVisible}
135+
closeModal={() => setModalVisible(false)}
136+
onConfigCreate={() => {
137+
setModalVisible(false);
138+
dispatch(fetchApiKeysAction());
139+
}}
140+
/>
141+
</>
142+
)
143+
}

‎client/packages/lowcoder/src/redux/reducers/uiReducers/usersReducer.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
ReduxActionErrorTypes,
44
ReduxActionTypes,
55
} from "constants/reduxActionConstants";
6-
import { CurrentUser, defaultCurrentUser, defaultUser, User } from "constants/userConstants";
6+
import { ApiKey, CurrentUser, defaultCurrentUser, defaultUser, User } from "constants/userConstants";
77
import { UpdateOrgPayload } from "redux/reduxActions/orgActions";
88
import { MarkUserStatusPayload, UpdateUserPayload } from "redux/reduxActions/userActions";
99
import { createReducer } from "util/reducerUtils";
@@ -20,6 +20,7 @@ const initialState: UsersReduxState = {
2020
currentUser: defaultCurrentUser,
2121
rawCurrentUser: defaultCurrentUser,
2222
profileSettingModalVisible: false,
23+
apiKeys: [],
2324
};
2425

2526
const usersReducer = createReducer(initialState, {
@@ -182,6 +183,13 @@ const usersReducer = createReducer(initialState, {
182183
},
183184
};
184185
},
186+
[ReduxActionTypes.FETCH_API_KEYS_SUCCESS]: (
187+
state: UsersReduxState,
188+
action: ReduxAction<Array<ApiKey>>
189+
): UsersReduxState => ({
190+
...state,
191+
apiKeys: action.payload,
192+
}),
185193
});
186194

187195
export interface UsersReduxState {
@@ -196,6 +204,7 @@ export interface UsersReduxState {
196204
};
197205
error: string;
198206
profileSettingModalVisible: boolean;
207+
apiKeys: Array<ApiKey>;
199208
}
200209

201210
export default usersReducer;

‎client/packages/lowcoder/src/redux/reduxActions/userActions.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ReduxAction, ReduxActionTypes } from "constants/reduxActionConstants";
2-
import { UserStatusType } from "constants/userConstants";
2+
import { ApiKey, UserStatusType } from "constants/userConstants";
33

44
export const fetchUserAction = () => ({
55
type: ReduxActionTypes.FETCH_USER_INIT,
@@ -20,6 +20,9 @@ export type UpdateUserPayload = {
2020
avatarUrl?: string;
2121
uiLanguage?: string;
2222
};
23+
24+
export type FetchApiKeysPayload = Array<ApiKey>;
25+
2326
export const updateUserAction = (payload: UpdateUserPayload) => {
2427
return {
2528
type: ReduxActionTypes.UPDATE_USER_PROFILE,
@@ -58,3 +61,16 @@ export const logoutAction = (payload: LogoutActionPayload) => ({
5861
export const logoutSuccess = () => ({
5962
type: ReduxActionTypes.LOGOUT_USER_SUCCESS,
6063
});
64+
65+
export const fetchApiKeysAction = () => {
66+
return {
67+
type: ReduxActionTypes.FETCH_API_KEYS,
68+
};
69+
};
70+
71+
export const fetchApiKeysSuccess = (payload: FetchApiKeysPayload) => {
72+
return {
73+
type: ReduxActionTypes.FETCH_API_KEYS_SUCCESS,
74+
payload: payload,
75+
};
76+
};

‎client/packages/lowcoder/src/redux/sagas/userSagas.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ApiResponse } from "api/apiResponses";
2-
import UserApi, { GetCurrentUserResponse, GetUserResponse } from "api/userApi";
2+
import UserApi, { FetchApiKeysResponse, GetCurrentUserResponse, GetUserResponse } from "api/userApi";
33
import { AxiosResponse } from "axios";
44
import {
55
ReduxAction,
@@ -177,6 +177,22 @@ function* markUserStatusSaga(action: ReduxAction<MarkUserStatusPayload>) {
177177
}
178178
}
179179

180+
export function* fetchApiKeysSaga() {
181+
try {
182+
const response: AxiosResponse<FetchApiKeysResponse> = yield call(UserApi.fetchApiKeys);
183+
const isValidResponse: boolean = validateResponse(response);
184+
if (isValidResponse) {
185+
const apiKeys = response.data.data;
186+
yield put({
187+
type: ReduxActionTypes.FETCH_API_KEYS_SUCCESS,
188+
payload: apiKeys,
189+
});
190+
}
191+
} catch(error: any) {
192+
log.error(error);
193+
}
194+
}
195+
180196
export default function* userSagas() {
181197
yield all([
182198
takeLatest(ReduxActionTypes.LOGOUT_USER_INIT, logoutSaga),
@@ -185,5 +201,6 @@ export default function* userSagas() {
185201
takeLatest(ReduxActionTypes.FETCH_RAW_CURRENT_USER, getRawCurrentUserSaga),
186202
takeLatest(ReduxActionTypes.UPDATE_USER_PROFILE, updateUserSaga),
187203
takeLatest(ReduxActionTypes.MARK_USER_STATUS, markUserStatusSaga),
204+
takeLatest(ReduxActionTypes.FETCH_API_KEYS, fetchApiKeysSaga),
188205
]);
189206
}

‎client/packages/lowcoder/src/redux/selectors/usersSelectors.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CurrentUser, User } from "constants/userConstants";
1+
import { ApiKey, CurrentUser, User } from "constants/userConstants";
22
import { AppState } from "redux/reducers";
33

44
export const getUser = (state: AppState): User => {
@@ -33,3 +33,7 @@ export const isProfileUpdating = (state: AppState): boolean => {
3333

3434
export const isProfileSettingModalVisible = (state: AppState) =>
3535
state.ui.users.profileSettingModalVisible;
36+
37+
export const getApiKeys = (state: AppState): Array<ApiKey> => {
38+
return state.ui.users.apiKeys;
39+
};

0 commit comments

Comments
 (0)
Please sign in to comment.