Skip to content

Commit cfb4682

Browse files
committed
Migrate apikeys to React
1 parent e0e266d commit cfb4682

File tree

7 files changed

+211
-131
lines changed

7 files changed

+211
-131
lines changed

src/apps/dashboard/routes/_asyncRoutes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
1010
{ path: 'users/add', type: AsyncRouteType.Dashboard },
1111
{ path: 'users/parentalcontrol', type: AsyncRouteType.Dashboard },
1212
{ path: 'users/password', type: AsyncRouteType.Dashboard },
13-
{ path: 'users/profile', type: AsyncRouteType.Dashboard }
13+
{ path: 'users/profile', type: AsyncRouteType.Dashboard },
14+
{ path: 'keys', type: AsyncRouteType.Dashboard }
1415
];

src/apps/dashboard/routes/_legacyRoutes.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
127127
controller: 'dashboard/scheduledtasks/scheduledtasks',
128128
view: 'dashboard/scheduledtasks/scheduledtasks.html'
129129
}
130-
}, {
131-
path: 'keys',
132-
pageProps: {
133-
controller: 'dashboard/apikeys',
134-
view: 'dashboard/apikeys.html'
135-
}
136130
}, {
137131
path: 'playback/streaming',
138132
pageProps: {

src/apps/dashboard/routes/keys.tsx

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import Page from 'components/Page';
2+
import SectionTitleContainer from 'elements/SectionTitleContainer';
3+
import { useApi } from 'hooks/useApi';
4+
import globalize from 'lib/globalize';
5+
import React, { useCallback, useEffect, useRef, useState } from 'react';
6+
import { getApiKeyApi } from '@jellyfin/sdk/lib/utils/api/api-key-api';
7+
import type { AuthenticationInfo } from '@jellyfin/sdk/lib/generated-client/models/authentication-info';
8+
import Loading from 'components/loading/LoadingComponent';
9+
import { Api } from '@jellyfin/sdk';
10+
import confirm from 'components/confirm/confirm';
11+
import ApiKeyCell from 'components/dashboard/apikeys/ApiKeyCell';
12+
13+
const ApiKeys = () => {
14+
const { api } = useApi();
15+
const [ keys, setKeys ] = useState<AuthenticationInfo[]>([]);
16+
const [ loading, setLoading ] = useState(true);
17+
const element = useRef<HTMLDivElement>(null);
18+
19+
const loadKeys = (currentApi: Api) => {
20+
return getApiKeyApi(currentApi)
21+
.getKeys()
22+
.then(({ data }) => {
23+
if (data.Items) {
24+
setKeys(data.Items);
25+
}
26+
})
27+
.catch((err) => {
28+
console.error('[apikeys] failed to load api keys', err);
29+
});
30+
};
31+
32+
const revokeKey = useCallback((accessToken: string) => {
33+
if (api) {
34+
confirm(globalize.translate('MessageConfirmRevokeApiKey'), globalize.translate('HeaderConfirmRevokeApiKey')).then(function () {
35+
setLoading(true);
36+
getApiKeyApi(api)
37+
.revokeKey({ key: accessToken })
38+
.then(() => loadKeys(api))
39+
.then(() => setLoading(false))
40+
.catch(err => {
41+
console.error('[apikeys] failed to revoke key', err);
42+
});
43+
}).catch(err => {
44+
console.error('[apikeys] failed to show confirmation dialog', err);
45+
});
46+
}
47+
}, [api]);
48+
49+
useEffect(() => {
50+
if (!api) {
51+
return;
52+
}
53+
54+
loadKeys(api).then(() => {
55+
setLoading(false);
56+
}).catch(err => {
57+
console.error('[apikeys] failed to load api keys', err);
58+
});
59+
60+
if (loading) {
61+
return;
62+
}
63+
64+
const page = element.current;
65+
66+
if (!page) {
67+
console.error('[apikeys] Unexpected null page reference');
68+
return;
69+
}
70+
71+
const showNewKeyPopup = () => {
72+
import('../../../components/prompt/prompt').then(({ default: prompt }) => {
73+
prompt({
74+
title: globalize.translate('HeaderNewApiKey'),
75+
label: globalize.translate('LabelAppName'),
76+
description: globalize.translate('LabelAppNameExample')
77+
}).then((value) => {
78+
getApiKeyApi(api)
79+
.createKey({ app: value })
80+
.then(() => loadKeys(api))
81+
.catch(err => {
82+
console.error('[apikeys] failed to create api key', err);
83+
});
84+
}).catch(() => {
85+
// popup closed
86+
});
87+
}).catch(err => {
88+
console.error('[apikeys] failed to load api key popup', err);
89+
});
90+
};
91+
92+
(page.querySelector('.btnNewKey') as HTMLButtonElement).addEventListener('click', showNewKeyPopup);
93+
94+
return () => {
95+
(page.querySelector('.btnNewKey') as HTMLButtonElement).removeEventListener('click', showNewKeyPopup);
96+
};
97+
}, [api, loading]);
98+
99+
if (loading) {
100+
return <Loading />;
101+
}
102+
103+
return (
104+
<Page
105+
id='apiKeysPage'
106+
title={globalize.translate('HeaderApiKeys')}
107+
className='mainAnimatedPage type-interior'
108+
>
109+
<div ref={element} className='content-primary'>
110+
<SectionTitleContainer
111+
title={globalize.translate('HeaderApiKeys')}
112+
isBtnVisible={true}
113+
btnId='btnAddSchedule'
114+
btnClassName='fab submit sectionTitleButton btnNewKey'
115+
btnTitle={globalize.translate('Add')}
116+
btnIcon='add'
117+
/>
118+
<p>{globalize.translate('HeaderApiKeysHelp')}</p>
119+
<br />
120+
<table className='tblApiKeys detailTable'>
121+
<caption className='clipForScreenReader'>{globalize.translate('ApiKeysCaption')}</caption>
122+
<thead>
123+
<tr>
124+
<th scope='col' className='detailTableHeaderCell'></th>
125+
<th scope='col' className='detailTableHeaderCell'>{globalize.translate('HeaderApiKey')}</th>
126+
<th scope='col' className='detailTableHeaderCell'>{globalize.translate('HeaderApp')}</th>
127+
<th scope='col' className='detailTableHeaderCell'>{globalize.translate('HeaderDateIssued')}</th>
128+
</tr>
129+
</thead>
130+
<tbody className='resultBody'>
131+
{keys.map(key => {
132+
return <ApiKeyCell key={key.AccessToken} apiKey={key} revokeKey={revokeKey} />;
133+
})}
134+
</tbody>
135+
</table>
136+
</div>
137+
</Page>
138+
);
139+
};
140+
141+
export default ApiKeys;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React, { FunctionComponent, useCallback } from 'react';
2+
import type { AuthenticationInfo } from '@jellyfin/sdk/lib/generated-client/models/authentication-info';
3+
import ButtonElement from 'elements/ButtonElement';
4+
import datetime from 'scripts/datetime';
5+
import globalize from 'lib/globalize';
6+
7+
type ApiKeyCellProps = {
8+
apiKey: AuthenticationInfo;
9+
revokeKey?: (accessToken: string) => void;
10+
};
11+
12+
const ApiKeyCell: FunctionComponent<ApiKeyCellProps> = ({ apiKey, revokeKey }: ApiKeyCellProps) => {
13+
const getDate = (dateCreated: string | undefined) => {
14+
const date = datetime.parseISO8601Date(dateCreated, true);
15+
return datetime.toLocaleDateString(date) + ' ' + datetime.getDisplayTime(date);
16+
};
17+
18+
const onClick = useCallback(() => {
19+
if (apiKey?.AccessToken && revokeKey !== undefined) {
20+
revokeKey(apiKey.AccessToken);
21+
}
22+
}, [apiKey, revokeKey]);
23+
24+
return (
25+
<tr className='detailTableBodyRow detailTableBodyRow-shaded apiKey'>
26+
<td className='detailTableBodyCell'>
27+
<ButtonElement
28+
className='raised raised-mini btnRevoke'
29+
title={globalize.translate('ButtonRevoke')}
30+
onClick={onClick}
31+
/>
32+
</td>
33+
<td className='detailTableBodyCell' style={{ verticalAlign: 'middle' }}>
34+
{apiKey.AccessToken}
35+
</td>
36+
<td className='detailTableBodyCell' style={{ verticalAlign: 'middle' }}>
37+
{apiKey.AppName}
38+
</td>
39+
<td className='detailTableBodyCell' style={{ verticalAlign: 'middle' }}>
40+
{getDate(apiKey.DateCreated)}
41+
</td>
42+
</tr>
43+
);
44+
};
45+
46+
export default ApiKeyCell;

src/controllers/dashboard/apikeys.html

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/controllers/dashboard/apikeys.js

Lines changed: 0 additions & 89 deletions
This file was deleted.

src/elements/ButtonElement.tsx

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,32 @@ type IProps = {
2222
title?: string;
2323
leftIcon?: string;
2424
rightIcon?: string;
25+
onClick?: () => void;
2526
};
2627

27-
const ButtonElement: FunctionComponent<IProps> = ({ type, id, className, title, leftIcon, rightIcon }: IProps) => {
28+
const ButtonElement: FunctionComponent<IProps> = ({ type, id, className, title, leftIcon, rightIcon, onClick }: IProps) => {
29+
const button = createButtonElement({
30+
type: type,
31+
id: id ? `id="${id}"` : '',
32+
className: className,
33+
title: globalize.translate(title),
34+
leftIcon: leftIcon ? `<span class="material-icons ${leftIcon}" aria-hidden="true"></span>` : '',
35+
rightIcon: rightIcon ? `<span class="material-icons ${rightIcon}" aria-hidden="true"></span>` : ''
36+
});
37+
38+
if (onClick !== undefined) {
39+
return (
40+
<button
41+
style={{ all: 'unset' }}
42+
dangerouslySetInnerHTML={button}
43+
onClick={onClick}
44+
/>
45+
);
46+
}
47+
2848
return (
2949
<div
30-
dangerouslySetInnerHTML={createButtonElement({
31-
type: type,
32-
id: id ? `id="${id}"` : '',
33-
className: className,
34-
title: globalize.translate(title),
35-
leftIcon: leftIcon ? `<span class="material-icons ${leftIcon}" aria-hidden="true"></span>` : '',
36-
rightIcon: rightIcon ? `<span class="material-icons ${rightIcon}" aria-hidden="true"></span>` : ''
37-
})}
50+
dangerouslySetInnerHTML={button}
3851
/>
3952
);
4053
};

0 commit comments

Comments
 (0)