Compare commits

..
7 Commits
Author SHA1 Message Date
Misha Bragin 7a29dac01c Add single line installer command for Mac and Linux (#162) 2023-04-04 14:20:02 +02:00
pascal-fischer 444e9ec44a fetch userID from user list and not oidc by default (#161) 2023-04-03 16:52:42 +02:00
pascal-fischer dff0313f82 Feature/pat support (#157)
* Add working UI + API calls [missing token popup]

* use popup view to add new token

* show userID if user name not available

* switch from description to name

* show "Me" instead of own name

* removed created_by column

* update add token explanation

* use object instead of plain text for token create response

* some style changes

* disable information button for tokens

* last_used can contain nil

* fix delete popups

* lower case letters for dates

* add activity and fix visibility

* show settings tab for non admins

* remove spaces on top of setting tabs

* fix copy button size and position

* fix list footers

* continue merge changes to new files
2023-04-03 12:29:40 +02:00
Maycon Santos 11fbfb336a Clean last estimated name (#160) 2023-04-03 12:16:12 +02:00
Maycon Santos 4a0ae8f27d Add missing config change (#159) 2023-04-01 21:58:46 +02:00
Maycon Santos 9a72d8b0c4 Allow defining API token source (#158)
On many IDP providers, the access token
 is used to access the IDP's own API

 With these changes, we allow users to define the proper token to be used for
 management API calls
2023-04-01 19:44:28 +02:00
Misha Bragin 8e038cf242 Make table headers font-weight normal (#156) 2023-03-27 15:59:18 +02:00
38 changed files with 1276 additions and 336 deletions

No files matched your search

+2 -1
View File
@@ -52,11 +52,12 @@ export AUTH_SUPPORTED_SCOPES=${AUTH_SUPPORTED_SCOPES:-openid profile email api o
export NETBIRD_MGMT_API_ENDPOINT=$(echo $NETBIRD_MGMT_API_ENDPOINT | sed -E 's/(:80|:443)$//')
export NETBIRD_MGMT_GRPC_API_ENDPOINT=${NETBIRD_MGMT_GRPC_API_ENDPOINT}
export NETBIRD_HOTJAR_TRACK_ID=${NETBIRD_HOTJAR_TRACK_ID}
export NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken}
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
# replace ENVs in the config
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI"
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE"
MAIN_JS=$(find /usr/share/nginx/html/static/js/main.*js)
OIDC_TRUSTED_DOMAINS="/usr/share/nginx/html/OidcTrustedDomains.js"
+6 -5
View File
@@ -16,7 +16,7 @@ import SetupKeys from "./views/SetupKeys";
import AccessControl from "./views/AccessControl";
import Users from "./views/Users";
import FooterComponent from "./components/FooterComponent";
import {useGetAccessTokenSilently} from "./utils/token";
import {useGetTokenSilently, useTokenSource} from "./utils/token";
import {User} from "./store/user/types";
import {SecureLoading} from "./components/Loading";
import DNS from "./views/DNS";
@@ -29,8 +29,9 @@ const {Header, Content} = Layout;
function App() {
const run = useRef(false)
const [show, setShow] = useState(false)
const {getAccessTokenSilently} = useGetAccessTokenSilently();
const {hotjarTrackID} = getConfig();
const {hotjarTrackID,tokenSource} = getConfig();
useTokenSource(tokenSource)
const {getTokenSilently} = useGetTokenSilently();
// @ts-ignore
if (hotjarTrackID && window._DATADOG_SYNTHETICS_BROWSER === undefined) {
hotjar.initialize(hotjarTrackID, 6);
@@ -55,7 +56,7 @@ function App() {
useEffect(() => {
if (!run.current) {
run.current = true
apiClient.request<User[]>('GET', `/api/users`, {getAccessTokenSilently: getAccessTokenSilently})
apiClient.request<User[]>('GET', `/api/users`, {getAccessTokenSilently: getTokenSilently})
.then(() => {
setShow(true)
})
@@ -65,7 +66,7 @@ function App() {
})
}
}, [getAccessTokenSilently])
}, [getTokenSilently])
return (
<>
+3 -3
View File
@@ -23,7 +23,7 @@ import {Rule, RuleToSave} from "../store/rule/types";
import {uniq} from "lodash"
import {Header} from "antd/es/layout/layout";
import {RuleObject} from "antd/lib/form";
import {useGetAccessTokenSilently} from "../utils/token";
import {useGetTokenSilently} from "../utils/token";
const {Paragraph} = Typography;
const {Option} = Select;
@@ -34,7 +34,7 @@ interface FormRule extends Rule {
}
const AccessControlNew = () => {
const {getAccessTokenSilently} = useGetAccessTokenSilently()
const {getTokenSilently} = useGetTokenSilently()
const dispatch = useDispatch()
const setupNewRuleVisible = useSelector((state: RootState) => state.rule.setupNewRuleVisible)
const groups = useSelector((state: RootState) => state.group.data)
@@ -103,7 +103,7 @@ const AccessControlNew = () => {
.then((values) => {
const ruleToSave = createRuleToSave()
dispatch(ruleActions.saveRule.request({
getAccessTokenSilently: getAccessTokenSilently,
getAccessTokenSilently: getTokenSilently,
payload: ruleToSave
}))
})
+3 -3
View File
@@ -32,7 +32,7 @@ import {RuleObject} from "antd/lib/form";
import cidrRegex from 'cidr-regex';
import {NameServer, NameServerGroup, NameServerGroupToSave} from "../store/nameservers/types";
import {useGetGroupTagHelpers} from "../utils/groups"
import {useGetAccessTokenSilently} from "../utils/token";
import {useGetTokenSilently} from "../utils/token";
const {Paragraph} = Typography;
@@ -51,7 +51,7 @@ const NameServerGroupUpdate = () => {
selectValidator
} = useGetGroupTagHelpers()
const dispatch = useDispatch()
const {getAccessTokenSilently} = useGetAccessTokenSilently()
const {getTokenSilently} = useGetTokenSilently()
const {Option} = Select;
const nsGroup = useSelector((state: RootState) => state.nameserverGroup.nameserverGroup)
const setupNewNameServerGroupVisible = useSelector((state: RootState) => state.nameserverGroup.setupNewNameServerGroupVisible)
@@ -220,7 +220,7 @@ const NameServerGroupUpdate = () => {
.then((values) => {
const nsGroupToSave = createNSGroupToSave(values as NameServerGroup)
dispatch(nsGroupActions.saveNameServerGroup.request({
getAccessTokenSilently: getAccessTokenSilently,
getAccessTokenSilently: getTokenSilently,
payload: nsGroupToSave
}))
+4 -4
View File
@@ -11,7 +11,7 @@ import {User} from "../store/user/types";
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "typesafe-actions";
import {actions as userActions} from "../store/user";
import {useGetAccessTokenSilently} from "../utils/token";
import {useGetTokenSilently} from "../utils/token";
const {useBreakpoint} = Grid;
@@ -19,7 +19,7 @@ const Navbar = () => {
let location = useLocation();
const config = getConfig();
const { logout } = useOidc();
const {getAccessTokenSilently} = useGetAccessTokenSilently()
const {getTokenSilently} = useGetTokenSilently()
const dispatch = useDispatch()
const {oidcUser} = useOidcUser();
@@ -47,7 +47,7 @@ const Navbar = () => {
const userEmailKey = 'user-email'
const userLogoutKey = 'user-logout'
const userDividerKey = 'user-divider'
const adminOnlyTabs = ["/setup-keys", "/acls", "/routes", "/dns", "/activity", "/settings"]
const adminOnlyTabs = ["/setup-keys", "/acls", "/routes", "/dns", "/activity"]
const [menuItems, setMenuItems] = useState(items)
const logoutWithRedirect = () =>
logout("/", {client_id: config.clientId});
@@ -80,7 +80,7 @@ const Navbar = () => {
window.location.pathname !== '/peers' &&
window.location.pathname !== '/users') {
setIsRefreshingUserState(true)
dispatch(userActions.getUsers.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}))
dispatch(userActions.getUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}))
return
}
if (users.length === 0 && isRefreshingUserState) {
+5 -4
View File
@@ -9,7 +9,7 @@ import {FormPeer, Peer, PeerGroupsToSave} from "../store/peer/types";
import {Group, GroupPeer} from "../store/group/types";
import {CloseOutlined, EditOutlined} from "@ant-design/icons";
import {RuleObject} from 'antd/lib/form';
import {useGetAccessTokenSilently} from "../utils/token";
import {useGetTokenSilently} from "../utils/token";
import {timeAgo} from "../utils/common";
const {Paragraph} = Typography;
@@ -19,7 +19,7 @@ const punycode = require('punycode/')
const PeerUpdate = () => {
const {getAccessTokenSilently} = useGetAccessTokenSilently()
const {getTokenSilently} = useGetTokenSilently()
const dispatch = useDispatch()
const groups = useSelector((state: RootState) => state.group.data)
const users = useSelector((state: RootState) => state.user.data)
@@ -191,6 +191,7 @@ const PeerUpdate = () => {
setCallingPeerAPI(false)
setCallingPeerAPI(false)
setSubmitRunning(false)
setEstimatedName("")
}
const noUpdateToGroups = (): Boolean => {
@@ -250,14 +251,14 @@ const PeerUpdate = () => {
const peerUpdate = createPeerToSave()
setCallingPeerAPI(true)
dispatch(peerActions.updatePeer.request({
getAccessTokenSilently: getAccessTokenSilently,
getAccessTokenSilently: getTokenSilently,
payload: peerUpdate
}))
}
if (!noUpdateToGroups()) {
setCallingGroupAPI(true)
dispatch(peerActions.saveGroups.request({
getAccessTokenSilently: getAccessTokenSilently,
getAccessTokenSilently: getTokenSilently,
payload: peerGroupsToSave
}))
}
+4 -4
View File
@@ -30,7 +30,7 @@ import {
routePeerSeparator,
transformGroupedDataTable
} from '../utils/routes'
import {useGetAccessTokenSilently} from "../utils/token";
import {useGetTokenSilently} from "../utils/token";
import {useGetGroupTagHelpers} from "../utils/groups";
const {Paragraph} = Typography;
@@ -50,7 +50,7 @@ const RouteUpdate = () => {
selectValidator
} = useGetGroupTagHelpers()
const {Option} = Select;
const {getAccessTokenSilently} = useGetAccessTokenSilently()
const {getTokenSilently} = useGetTokenSilently()
const dispatch = useDispatch()
const setupNewRouteVisible = useSelector((state: RootState) => state.route.setupNewRouteVisible)
const setupNewRouteHA = useSelector((state: RootState) => state.route.setupNewRouteHA)
@@ -163,7 +163,7 @@ const RouteUpdate = () => {
if (!setupNewRouteHA || formRoute.peer != '') {
const routeToSave = createRouteToSave(formRoute)
dispatch(routeActions.saveRoute.request({
getAccessTokenSilently: getAccessTokenSilently,
getAccessTokenSilently: getTokenSilently,
payload: routeToSave
}))
} else {
@@ -180,7 +180,7 @@ const RouteUpdate = () => {
}
const routeToSave = createRouteToSave(updateRoute)
dispatch(routeActions.saveRoute.request({
getAccessTokenSilently: getAccessTokenSilently,
getAccessTokenSilently: getTokenSilently,
payload: routeToSave
}))
})
+3 -3
View File
@@ -27,7 +27,7 @@ import {checkExpiresIn, formatDate, timeAgo} from "../utils/common";
import {RuleObject} from "antd/lib/form";
import {CustomTagProps} from "rc-select/lib/BaseSelect";
import {Group} from "../store/group/types";
import {useGetAccessTokenSilently} from "../utils/token";
import {useGetTokenSilently} from "../utils/token";
import ExpiresInInput, {expiresInToSeconds, ExpiresInValue} from "../views/ExpiresInInput";
import moment from "moment";
@@ -54,7 +54,7 @@ const customLastUsedFormat: DatePickerProps['format'] = value => {
}
const SetupKeyNew = () => {
const {getAccessTokenSilently} = useGetAccessTokenSilently()
const {getTokenSilently} = useGetTokenSilently()
const dispatch = useDispatch()
const setupNewKeyVisible = useSelector((state: RootState) => state.setupKey.setupNewKeyVisible)
const setupKey = useSelector((state: RootState) => state.setupKey.setupKey)
@@ -127,7 +127,7 @@ const SetupKeyNew = () => {
.then((values) => {
let setupKeyToSave = createSetupKeyToSave()
dispatch(setupKeyActions.saveSetupKey.request({
getAccessTokenSilently: getAccessTokenSilently,
getAccessTokenSilently: getTokenSilently,
payload: setupKeyToSave
}))
})
+4 -4
View File
@@ -9,7 +9,7 @@ import {FormUser, User, UserToSave} from "../store/user/types";
import {RuleObject} from "antd/lib/form";
import {CustomTagProps} from "rc-select/lib/BaseSelect";
import {actions as userActions} from "../store/user";
import {useGetAccessTokenSilently} from "../utils/token";
import {useGetTokenSilently} from "../utils/token";
import {useOidcUser} from "@axa-fr/react-oidc";
const {Paragraph} = Typography;
@@ -20,7 +20,7 @@ const {Option} = Select;
const UserUpdate = () => {
const {oidcUser} = useOidcUser();
const {getAccessTokenSilently} = useGetAccessTokenSilently()
const {getTokenSilently} = useGetTokenSilently()
const dispatch = useDispatch()
const user = useSelector((state: RootState) => state.user.user)
const savedUser = useSelector((state: RootState) => state.user.savedUser)
@@ -193,7 +193,7 @@ const UserUpdate = () => {
okType: 'danger',
onOk() {
dispatch(userActions.saveUser.request({
getAccessTokenSilently: getAccessTokenSilently,
getAccessTokenSilently: getTokenSilently,
payload: userToSave
}))
},
@@ -215,7 +215,7 @@ const UserUpdate = () => {
showConfirmChangeRole(userToSave)
} else {
dispatch(userActions.saveUser.request({
getAccessTokenSilently: getAccessTokenSilently,
getAccessTokenSilently: getTokenSilently,
payload: userToSave
}))
}
+1 -1
View File
@@ -38,7 +38,7 @@ export const AddPeerPopup: React.FC<Props> = ({
const items: TabsProps['items'] = [
{
key: "1",
label: <span><Icon component={LinuxSVG}/>Ubuntu</span>,
label: <span><Icon component={LinuxSVG}/>Linux</span>,
children: <UbuntuTab/>,
},
{
+18 -15
View File
@@ -5,6 +5,7 @@ import TabSteps from "./TabSteps";
import {StepCommand} from "./types"
import {formatNetBirdUP} from "./common"
import {Collapse} from "antd";
import SyntaxHighlighter from "react-syntax-highlighter";
const { Panel } = Collapse;
const {Text} = Typography;
@@ -79,31 +80,33 @@ export const LinuxTab = () => {
])
return (
/*<div style={{marginTop: 10}}>
<div style={{marginTop: 10}}>
<Text style={{fontWeight: "bold"}}>
Install on macOS
Install with one command
</Text>
<div style={{fontSize: ".85em", marginTop: 5, marginBottom: 25}}>
<SyntaxHighlighter language="bash">
curl -fsSL https://pkgs.netbird.io/install.sh | sh
</SyntaxHighlighter>
</div>
<Text style={{fontWeight: "bold"}}>
Or install manually with HomeBrew
</Text>
<div style={{marginTop: 5}}>
<TabSteps stepsItems={quickSteps}/>
<TabSteps stepsItems={steps}/>
</div>
<Collapse style={{marginLeft: -15}} bordered={false}>
<Panel header={<Text style={{fontWeight: "bold"}}>
Or install manually with Homebrew
</Text>} key="1">
<div style={{marginTop: 5}}>
<TabSteps stepsItems={steps}/>
</div>
</Panel>
</Collapse>
</div>*/
<div style={{marginTop: 10}}>
</div>
/*<div style={{marginTop: 5}}>
<TabSteps stepsItems={quickSteps}/>
</div>*/
/*<div style={{marginTop: 10}}>
<Text style={{fontWeight: "bold"}}>
Install on macOS with Homebrew
</Text>
<div style={{marginTop: 5}}>
<TabSteps stepsItems={steps}/>
</div>
</div>
</div>*/
)
}
+4 -7
View File
@@ -45,19 +45,16 @@ export const UbuntuTab = () => {
return (
<div style={{marginTop: 10}}>
{/*<Text style={{fontWeight: "bold"}}>
<Text style={{fontWeight: "bold"}}>
Install with one command
</Text>
<div style={{fontSize: ".85em", marginTop: 5, marginBottom: 25}}>
<SyntaxHighlighter language="bash">
curl -fsSL https://netbird.io/install.sh | sh
curl -fsSL https://pkgs.netbird.io/install.sh | sh
</SyntaxHighlighter>
</div>*/}
{/*<Text style={{fontWeight: "bold"}}>*/}
{/* Or install manually*/}
{/*</Text>*/}
</div>
<Text style={{fontWeight: "bold"}}>
Install on Ubuntu
Or install manually on Ubuntu
</Text>
<div style={{marginTop: 5}}>
<TabSteps stepsItems={steps}/>
+2 -1
View File
@@ -9,5 +9,6 @@
"grpcApiOrigin": "$NETBIRD_MGMT_GRPC_API_ENDPOINT",
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
"redirectURI": "$AUTH_REDIRECT_URI",
"silentRedirectURI": "$AUTH_SILENT_REDIRECT_URI"
"silentRedirectURI": "$AUTH_SILENT_REDIRECT_URI",
"tokenSource": "$NETBIRD_TOKEN_SOURCE"
}
+7 -1
View File
@@ -9,7 +9,7 @@ if (process.env.NODE_ENV !== 'production') {
const defaultRedirectURI = '/#callback';
const defaultSilentRedirectURI = '/#silent-callback'
const defaultTokenSource = "accessToken"
export function getConfig() {
let redirectURI = defaultRedirectURI
if (configJson.redirectURI) {
@@ -21,6 +21,11 @@ export function getConfig() {
silentRedirectURI = configJson.silentRedirectURI
}
let tokenSource = defaultTokenSource
if (configJson.tokenSource) {
tokenSource = configJson.tokenSource
}
return {
auth0Auth: configJson.auth0Auth == "true", //due to substitution we can't use boolean in the config
authority: configJson.authAuthority,
@@ -32,5 +37,6 @@ export function getConfig() {
hotjarTrackID: configJson.hotjarTrackID,
redirectURI: redirectURI,
silentRedirectURI: silentRedirectURI,
tokenSource: tokenSource,
};
}
+4
View File
@@ -139,4 +139,8 @@ body {
.ant-steps-item-tail {
border: none;
}
.ant-table-thead .ant-table-cell {
font-weight: normal !important;
}
+2
View File
@@ -12,6 +12,7 @@ import { sagas as nameserverGroupSagas } from './nameservers';
import { sagas as eventSagas } from './event';
import { sagas as dnsSettingsSagas } from './dns-settings';
import { sagas as accountSagas } from './account';
import { sagas as personalAccessTokenSagas } from './personal-access-token';
import rootReducer from './root-reducer';
import { apiClient } from '../services/api-client';
@@ -33,5 +34,6 @@ sagaMiddleware.run(nameserverGroupSagas);
sagaMiddleware.run(eventSagas);
sagaMiddleware.run(dnsSettingsSagas);
sagaMiddleware.run(accountSagas);
sagaMiddleware.run(personalAccessTokenSagas);
export { apiClient, rootReducer, store };
@@ -0,0 +1,39 @@
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
import {PersonalAccessToken, PersonalAccessTokenCreate, PersonalAccessTokenGenerated, SpecificPAT} from './types';
import {
ApiError,
CreateResponse,
DeleteResponse,
RequestPayload
} from '../../services/api-client/types';
const actions = {
getPersonalAccessTokens: createAsyncAction(
'GET_PERSONAL_ACCESS_TOKEN_REQUEST',
'GET_PERSONAL_ACCESS_TOKEN_SUCCESS',
'GET_PERSONAL_ACCESS_TOKEN_FAILURE',
)<RequestPayload<string>, PersonalAccessToken[], ApiError>(),
savePersonalAccessToken: createAsyncAction(
'SAVE_PERSONAL_ACCESS_TOKEN_REQUEST',
'SAVE_PERSONAL_ACCESS_TOKEN_SUCCESS',
'SAVE_PERSONAL_ACCESS_TOKEN_FAILURE',
)<RequestPayload<PersonalAccessTokenCreate>, CreateResponse<PersonalAccessTokenGenerated | null>, CreateResponse<PersonalAccessTokenGenerated | null>>(),
setSavedPersonalAccessToken: createAction('SET_PERSONAL_ACCESS_TOKEN_KEY')<CreateResponse<PersonalAccessTokenGenerated | null>>(),
resetSavedPersonalAccessToken: createAction('RESET_PERSONAL_ACCESS_TOKEN_KEY')<null>(),
deletePersonalAccessToken: createAsyncAction(
'DELETE_PERSONAL_ACCESS_TOKEN_REQUEST',
'DELETE_PERSONAL_ACCESS_TOKEN_SUCCESS',
'DELETE_PERSONAL_ACCESS_TOKEN_FAILURE'
)<RequestPayload<SpecificPAT>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
setDeletePersonalAccessToken: createAction('SET_DELETE_PERSONAL_ACCESS_TOKEN')<DeleteResponse<string | null>>(),
resetDeletedPersonalAccessToken: createAction('RESET_DELETE_PERSONAL_ACCESS_TOKEN')<null>(),
removePersonalAccessToken: createAction('REMOVE_PERSONAL_ACCESS_TOKEN')<string>(),
setPersonalAccessToken: createAction('SET_SETUP_KEY')<PersonalAccessTokenCreate>(),
setNewPersonalAccessTokenVisible: createAction('SET_NEW_PERSONAL_ACCESS_TOKEN_VISIBLE')<boolean>()
};
export type ActionTypes = ActionType<typeof actions>;
export default actions;
+7
View File
@@ -0,0 +1,7 @@
import actions, { ActionTypes as _actionTypes } from './actions';
import reducer from './reducer';
import sagas from './sagas';
export type ActionTypes = _actionTypes;
export { actions, reducer, sagas };
@@ -0,0 +1,97 @@
import { createReducer } from 'typesafe-actions';
import { combineReducers } from 'redux';
import actions, { ActionTypes } from './actions';
import {ApiError, DeleteResponse, CreateResponse, ChangeResponse} from "../../services/api-client/types";
import {PersonalAccessToken, PersonalAccessTokenCreate, PersonalAccessTokenGenerated} from "./types";
type StateType = Readonly<{
data: PersonalAccessToken[] | null;
personalAccessToken: PersonalAccessTokenCreate | null;
loading: boolean;
failed: ApiError | null;
saving: boolean;
deletedPersonalAccessToken: DeleteResponse<string | null>;
revokedPersonalAccessToken: ChangeResponse<string | null>;
savedPersonalAccessToken: CreateResponse<PersonalAccessTokenGenerated | null>;
newPersonalAccessTokenVisible: boolean
}>;
const initialState: StateType = {
data: [],
personalAccessToken: null,
loading: false,
failed: null,
saving: false,
deletedPersonalAccessToken: <DeleteResponse<string | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
},
revokedPersonalAccessToken: <ChangeResponse<string | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
},
savedPersonalAccessToken: <CreateResponse<PersonalAccessTokenGenerated | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
},
newPersonalAccessTokenVisible: false
};
const data = createReducer<PersonalAccessToken[], ActionTypes>(initialState.data as PersonalAccessToken[])
.handleAction(actions.getPersonalAccessTokens.success,(_, action) => action.payload)
.handleAction(actions.getPersonalAccessTokens.failure, () => []);
const personalAccessToken = createReducer<PersonalAccessTokenCreate, ActionTypes>(initialState.personalAccessToken as PersonalAccessTokenCreate)
.handleAction(actions.setPersonalAccessToken, (store, action) => action.payload);
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
.handleAction(actions.getPersonalAccessTokens.request, () => true)
.handleAction(actions.getPersonalAccessTokens.success, () => false)
.handleAction(actions.getPersonalAccessTokens.failure, () => false);
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
.handleAction(actions.getPersonalAccessTokens.request, () => null)
.handleAction(actions.getPersonalAccessTokens.success, () => null)
.handleAction(actions.getPersonalAccessTokens.failure, (store, action) => action.payload);
const saving = createReducer<boolean, ActionTypes>(initialState.saving)
.handleAction(actions.getPersonalAccessTokens.request, () => true)
.handleAction(actions.getPersonalAccessTokens.success, () => false)
.handleAction(actions.getPersonalAccessTokens.failure, () => false);
const deletedPersonalAccessToken = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deletedPersonalAccessToken)
.handleAction(actions.deletePersonalAccessToken.request, () => initialState.deletedPersonalAccessToken)
.handleAction(actions.deletePersonalAccessToken.success, (store, action) => action.payload)
.handleAction(actions.deletePersonalAccessToken.failure, (store, action) => action.payload)
.handleAction(actions.setDeletePersonalAccessToken, (store, action) => action.payload)
.handleAction(actions.resetDeletedPersonalAccessToken, (store, action) => initialState.deletedPersonalAccessToken);
const savedPersonalAccessToken = createReducer<CreateResponse<PersonalAccessTokenGenerated | null>, ActionTypes>(initialState.savedPersonalAccessToken)
.handleAction(actions.savePersonalAccessToken.request, () => initialState.savedPersonalAccessToken)
.handleAction(actions.savePersonalAccessToken.success, (store, action) => action.payload)
.handleAction(actions.savePersonalAccessToken.failure, (store, action) => action.payload)
.handleAction(actions.setSavedPersonalAccessToken, (store, action) => action.payload)
.handleAction(actions.resetSavedPersonalAccessToken, () => initialState.savedPersonalAccessToken)
const newPersonalAccessTokenVisible = createReducer<boolean, ActionTypes>(initialState.newPersonalAccessTokenVisible)
.handleAction(actions.setNewPersonalAccessTokenVisible, (store, action) => action.payload)
export default combineReducers({
data,
personalAccessToken: personalAccessToken,
loading,
failed,
saving,
deletedPersonalAccessToken: deletedPersonalAccessToken,
savedPersonalAccessToken: savedPersonalAccessToken,
newPersonalAccessTokenVisible: newPersonalAccessTokenVisible
});
+101
View File
@@ -0,0 +1,101 @@
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
import {ApiError, ApiResponse, CreateResponse, DeleteResponse} from '../../services/api-client/types';
import {PersonalAccessToken, PersonalAccessTokenCreate, PersonalAccessTokenGenerated} from './types'
import service from './service';
import actions from './actions';
export function* getPersonalAccessTokens(action: ReturnType<typeof actions.getPersonalAccessTokens.request>): Generator {
try {
const effect = yield call(service.getAllPersonalAccessTokens, action.payload);
const response = effect as ApiResponse<PersonalAccessToken[]>;
yield put(actions.getPersonalAccessTokens.success(response.body));
} catch (err) {
yield put(actions.getPersonalAccessTokens.failure(err as ApiError));
}
}
export function* savePersonalAccessToken(action: ReturnType<typeof actions.savePersonalAccessToken.request>): Generator {
try {
yield put(actions.setSavedPersonalAccessToken({
loading: true,
success: false,
failure: false,
error: null,
data: null
} as CreateResponse<PersonalAccessTokenGenerated | null>))
const tokenToSave = action.payload.payload
let effect = yield call(service.createPersonalAccessToken, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: {
user_id: tokenToSave.user_id,
name: tokenToSave.name,
expires_in: tokenToSave.expires_in,
} as PersonalAccessTokenCreate
});
const response = effect as ApiResponse<PersonalAccessTokenGenerated>;
yield put(actions.savePersonalAccessToken.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as CreateResponse<PersonalAccessTokenGenerated | null>));
yield put(actions.getPersonalAccessTokens.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: tokenToSave.user_id }));
} catch (err) {
yield put(actions.savePersonalAccessToken.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null
} as CreateResponse<PersonalAccessTokenGenerated | null>));
}
}
export function* deletePersonalAccessToken(action: ReturnType<typeof actions.deletePersonalAccessToken.request>): Generator {
try {
yield call(actions.setDeletePersonalAccessToken,{
loading: true,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>)
const effect = yield call(service.deletePersonalAccessToken, action.payload);
const response = effect as ApiResponse<any>;
yield put(actions.deletePersonalAccessToken.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as DeleteResponse<string | null>));
const personalAccessTokens = (yield select(state => state.personalAccessToken.data)) as PersonalAccessToken[]
yield put(actions.getPersonalAccessTokens.success(personalAccessTokens.filter((p:PersonalAccessToken) => p.id !== action.payload.payload.id)))
} catch (err) {
yield put(actions.deletePersonalAccessToken.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null
} as DeleteResponse<string | null>));
}
}
export default function* sagas(): Generator {
yield all([
takeLatest(actions.getPersonalAccessTokens.request, getPersonalAccessTokens),
takeLatest(actions.savePersonalAccessToken.request, savePersonalAccessToken),
takeLatest(actions.deletePersonalAccessToken.request, deletePersonalAccessToken)
]);
}
Loaded 20 of 38 files, more files were not shown because too many files have changed in this diff. Show more