Skip to content

Commit

Permalink
Migrate apikeys to React
Browse files Browse the repository at this point in the history
  • Loading branch information
viown committed Dec 16, 2024
1 parent e0e266d commit c4f1711
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 131 deletions.
3 changes: 2 additions & 1 deletion src/apps/dashboard/routes/_asyncRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
{ path: 'users/add', type: AsyncRouteType.Dashboard },
{ path: 'users/parentalcontrol', type: AsyncRouteType.Dashboard },
{ path: 'users/password', type: AsyncRouteType.Dashboard },
{ path: 'users/profile', type: AsyncRouteType.Dashboard }
{ path: 'users/profile', type: AsyncRouteType.Dashboard },
{ path: 'keys', type: AsyncRouteType.Dashboard }
];
6 changes: 0 additions & 6 deletions src/apps/dashboard/routes/_legacyRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
controller: 'dashboard/scheduledtasks/scheduledtasks',
view: 'dashboard/scheduledtasks/scheduledtasks.html'
}
}, {
path: 'keys',
pageProps: {
controller: 'dashboard/apikeys',
view: 'dashboard/apikeys.html'
}
}, {
path: 'playback/streaming',
pageProps: {
Expand Down
141 changes: 141 additions & 0 deletions src/apps/dashboard/routes/keys.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import Page from 'components/Page';
import SectionTitleContainer from 'elements/SectionTitleContainer';
import { useApi } from 'hooks/useApi';
import globalize from 'lib/globalize';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { getApiKeyApi } from '@jellyfin/sdk/lib/utils/api/api-key-api';
import { AuthenticationInfo } from '@jellyfin/sdk/lib/generated-client';
import Loading from 'components/loading/LoadingComponent';
import { Api } from '@jellyfin/sdk';
import confirm from 'components/confirm/confirm';
import ApiKeyCell from 'components/dashboard/apikeys/ApiKeyCell';

const ApiKeys = () => {
const { api } = useApi();
const [ keys, setKeys ] = useState<AuthenticationInfo[]>([]);
const [ loading, setLoading ] = useState(true);
const element = useRef<HTMLDivElement>(null);

const loadKeys = (currentApi: Api) => {
return getApiKeyApi(currentApi)
.getKeys()
.then(({ data }) => {
if (data.Items) {
setKeys(data.Items);
}
})
.catch((err) => {
console.error('[apikeys] failed to load api keys', err);
});
};

const revokeKey = useCallback((accessToken: string) => {
if (api) {
confirm(globalize.translate('MessageConfirmRevokeApiKey'), globalize.translate('HeaderConfirmRevokeApiKey')).then(function () {
setLoading(true);
getApiKeyApi(api)
.revokeKey({ key: accessToken })
.then(() => loadKeys(api))
.then(() => setLoading(false))
.catch(err => {
console.error('[apikeys] failed to revoke key', err);
});
}).catch(err => {
console.error('[apikeys] failed to show confirmation dialog', err);
});
}
}, [api]);

useEffect(() => {
if (!api) {
return;
}

loadKeys(api).then(() => {
setLoading(false);
}).catch(err => {
console.error('[apikeys] failed to load api keys', err);
});

if (loading) {
return;
}

const page = element.current;

if (!page) {
console.error('[apikeys] Unexpected null page reference');
return;
}

const showNewKeyPopup = () => {
import('../../../components/prompt/prompt').then(({ default: prompt }) => {
prompt({
title: globalize.translate('HeaderNewApiKey'),
label: globalize.translate('LabelAppName'),
description: globalize.translate('LabelAppNameExample')
}).then((value) => {
getApiKeyApi(api)
.createKey({ app: value })
.then(() => loadKeys(api))
.catch(err => {
console.error('[apikeys] failed to create api key', err);
});
}).catch(() => {
// popup closed
});
}).catch(err => {
console.error('[apikeys] failed to load api key popup', err);
});
};

(page.querySelector('.btnNewKey') as HTMLButtonElement).addEventListener('click', showNewKeyPopup);

return () => {
(page.querySelector('.btnNewKey') as HTMLButtonElement).removeEventListener('click', showNewKeyPopup);
};
}, [api, loading]);

if (loading) {
return <Loading />;
}

return (
<Page
id='apiKeysPage'
title={globalize.translate('HeaderApiKeys')}
className='mainAnimatedPage type-interior'
>
<div ref={element} className='content-primary'>
<SectionTitleContainer
title={globalize.translate('HeaderApiKeys')}
isBtnVisible={true}
btnId='btnAddSchedule'
btnClassName='fab submit sectionTitleButton btnNewKey'
btnTitle={globalize.translate('Add')}
btnIcon='add'
/>
<p>{globalize.translate('HeaderApiKeysHelp')}</p>
<br />
<table className='tblApiKeys detailTable'>
<caption className='clipForScreenReader'>{globalize.translate('ApiKeysCaption')}</caption>
<thead>
<tr>
<th scope='col' className='detailTableHeaderCell'></th>
<th scope='col' className='detailTableHeaderCell'>{globalize.translate('HeaderApiKey')}</th>
<th scope='col' className='detailTableHeaderCell'>{globalize.translate('HeaderApp')}</th>
<th scope='col' className='detailTableHeaderCell'>{globalize.translate('HeaderDateIssued')}</th>
</tr>
</thead>
<tbody className='resultBody'>
{keys.map(key => {
return <ApiKeyCell key={key.AccessToken} apiKey={key} revokeKey={revokeKey} />;
})}
</tbody>
</table>
</div>
</Page>
);
};

export default ApiKeys;
44 changes: 44 additions & 0 deletions src/components/dashboard/apikeys/ApiKeyCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { FunctionComponent, useCallback } from 'react';
import { AuthenticationInfo } from '@jellyfin/sdk/lib/generated-client';
import ButtonElement from 'elements/ButtonElement';
import datetime from 'scripts/datetime';
import globalize from 'lib/globalize';

type ApiKeyCellProps = {
apiKey: AuthenticationInfo;
revokeKey?: (accessToken: string) => void;
};

const ApiKeyCell: FunctionComponent<ApiKeyCellProps> = ({ apiKey, revokeKey }: ApiKeyCellProps) => {
const getDate = (dateCreated: string | undefined) => {
const date = datetime.parseISO8601Date(dateCreated, true);
return datetime.toLocaleDateString(date) + ' ' + datetime.getDisplayTime(date);
};

const onClick = useCallback(() => {
apiKey?.AccessToken && revokeKey !== undefined && revokeKey(apiKey.AccessToken);
}, [apiKey, revokeKey]);

return (
<tr className='detailTableBodyRow detailTableBodyRow-shaded apiKey'>
<td className='detailTableBodyCell'>
<ButtonElement
className='raised raised-mini btnRevoke'
title={globalize.translate('ButtonRevoke')}
onClick={onClick}
/>
</td>
<td className='detailTableBodyCell' style={{ verticalAlign: 'middle' }}>
{apiKey.AccessToken}
</td>
<td className='detailTableBodyCell' style={{ verticalAlign: 'middle' }}>
{apiKey.AppName}
</td>
<td className='detailTableBodyCell' style={{ verticalAlign: 'middle' }}>
{getDate(apiKey.DateCreated)}
</td>
</tr>
);
};

export default ApiKeyCell;
26 changes: 0 additions & 26 deletions src/controllers/dashboard/apikeys.html

This file was deleted.

89 changes: 0 additions & 89 deletions src/controllers/dashboard/apikeys.js

This file was deleted.

31 changes: 22 additions & 9 deletions src/elements/ButtonElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,32 @@ type IProps = {
title?: string;
leftIcon?: string;
rightIcon?: string;
onClick?: () => void;
};

const ButtonElement: FunctionComponent<IProps> = ({ type, id, className, title, leftIcon, rightIcon }: IProps) => {
const ButtonElement: FunctionComponent<IProps> = ({ type, id, className, title, leftIcon, rightIcon, onClick }: IProps) => {
const button = createButtonElement({
type: type,
id: id ? `id="${id}"` : '',
className: className,
title: globalize.translate(title),
leftIcon: leftIcon ? `<span class="material-icons ${leftIcon}" aria-hidden="true"></span>` : '',
rightIcon: rightIcon ? `<span class="material-icons ${rightIcon}" aria-hidden="true"></span>` : ''
});

if (onClick !== undefined) {
return (
<button
style={{ all: 'unset' }}
dangerouslySetInnerHTML={button}
onClick={onClick}
/>
);
}

return (
<div
dangerouslySetInnerHTML={createButtonElement({
type: type,
id: id ? `id="${id}"` : '',
className: className,
title: globalize.translate(title),
leftIcon: leftIcon ? `<span class="material-icons ${leftIcon}" aria-hidden="true"></span>` : '',
rightIcon: rightIcon ? `<span class="material-icons ${rightIcon}" aria-hidden="true"></span>` : ''
})}
dangerouslySetInnerHTML={button}
/>
);
};
Expand Down

0 comments on commit c4f1711

Please sign in to comment.