Skip to content

Commit

Permalink
[ALS-5911] Api Page, refactor
Browse files Browse the repository at this point in the history
[ALS-5911] Api Page

Add tests

refactor

More refactors

Fix tests
  • Loading branch information
JamesPeck committed Mar 7, 2024
1 parent e44992b commit c926c3c
Show file tree
Hide file tree
Showing 42 changed files with 640 additions and 119 deletions.
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@floating-ui/dom": "1.5.3",
"@fortawesome/fontawesome-free": "^6.5.1",
"@vincjo/datatables": "^1.14.5",
"dotenv": "^16.3.1"
"dotenv": "^16.3.1",
"svelte-eslint-parser": "^0.33.1"
}
}
3 changes: 3 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ const config: PlaywrightTestConfig = {
testDir: 'tests',
testMatch: /(.+\.)?(test|spec)\.[jt]s/,
reporter: [['list'], ['html']],
use: {
permissions: ['clipboard-read', 'clipboard-write'],
},
};

export default config;
30 changes: 30 additions & 0 deletions src/app.postcss
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,24 @@ nav#page-navigation a:active {
color: rgb(var(--theme-font-color-base));
}

#landing input[type='search'] {
width: 100%;
box-sizing: border-box;
margin-right: 0.5rem;
@apply input;
}

#landing .stats {
display: flex;
flex-direction: row;
justify-content: space-evenly;
align-items: center;
width: 70%;
margin: 2rem 0;
padding: 2rem;
@apply card variant-filled-primary;
}

.subtitle {
font-size: 2rem;
}
Expand Down Expand Up @@ -189,3 +207,15 @@ nav#page-navigation a:active {
.table.align-middle tbody td {
vertical-align: middle;
}

#user-token span.badge.expired {
@apply variant-filled-error;
}

#user-token span.badge.expiring {
@apply variant-filled-warning;
}

#user-token span.badge.valid {
@apply variant-filled-success;
}
5 changes: 2 additions & 3 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@ import { browser } from '$app/environment';

// TODO: fix any types
/* eslint-disable @typescript-eslint/no-explicit-any */

async function send({
method,
path,
data
data,
}: {
method: string;
path: string;
data?: any; //TODO: Change this
}) {
const opts: { method: string; headers: { [key: string]: string }; body?: string } = {
method,
headers: {}
headers: {},
};

if (data) {
Expand Down
31 changes: 0 additions & 31 deletions src/lib/component/dataset/cell/copy-button.svelte

This file was deleted.

File renamed without changes.
29 changes: 29 additions & 0 deletions src/lib/components/CopyButton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script lang="ts">
import { clipboard } from '@skeletonlabs/skeleton';
export let buttonText = 'Copy';
export let itemToCopy: string;
export let classes: string[] = ['variant-ringed-primary'];
let timer: ReturnType<typeof setTimeout>;
function debounce(text: string) {
clearTimeout(timer);
timer = setTimeout(() => {
buttonText = text;
}, 4500);
}
function updateCopyButtonText() {
debounce(buttonText);
buttonText = 'Copied!';
}
</script>

<button
type="button"
data-testid="copy-button"
class={'btn ' + classes.join(' ')}
on:click={updateCopyButtonText}
use:clipboard={itemToCopy}
>
{buttonText}
</button>
File renamed without changes.
File renamed without changes.
204 changes: 204 additions & 0 deletions src/lib/components/UserToken.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
<script lang="ts">
import {
type ToastSettings,
type ModalSettings,
getModalStore,
getToastStore,
ProgressRadial,
} from '@skeletonlabs/skeleton';
import * as api from '$lib/api';
import type { User } from '../models/User';
import CopyButton from './CopyButton.svelte';
import ErrorAlert from './ErrorAlert.svelte';
type LongTermTokenResponse = {
userLongTermToken: string;
};
const modalStore = getModalStore();
const toastStore = getToastStore();
let rows = 1;
let account = '';
let placeHolderToken =
'•••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••';
let textarea: HTMLTextAreaElement;
let revealed = false;
let refreshButtonDisabled = false;
let refreshButtonText = 'Refresh';
let user: User;
$: displayedToken = revealed ? user?.token : placeHolderToken;
$: revealButtonText = revealed ? 'Hide' : 'Reveal';
$: expires = user && user.token ? extractExperationDate(user.token) : 0;
$: badge = expires && checkIfExpired();
$: account = user?.email || 'There was an error retrieving your account.';
function checkIfExpired() {
if (!expires || expires === 0) {
return 'unknown';
}
const daysLeftOnToken = Math.floor((expires - Date.now()) / (1000 * 60 * 60 * 24));
if (expires < Date.now()) {
return 'expired';
} else if (daysLeftOnToken < 7) {
return 'expiring soon';
} else {
return 'valid for ' + daysLeftOnToken + ' more days';
}
}
function confirmRefreshToken() {
const modal: ModalSettings = {
type: 'confirm',
title: 'Please Confirm',
body: 'Are you sure you wish to invalidate your old token and create a new one?',
response: (r: boolean) => r && refreshToken(),
};
modalStore.trigger(modal);
}
function extractExperationDate(token: string) {
if (!token) return 0;
try {
return JSON.parse(atob(token.split('.')[1])).exp * 1000;
} catch (error) {
console.error(error);
const extractExperationDateErrorToast: ToastSettings = {
message:
'An error occured while parsing your token. Please try again later. If this problem persists, please contact an administrator.',
background: 'variant-filled-error',
};
toastStore.trigger(extractExperationDateErrorToast);
return 0; //TODO: Handle errors
}
}
async function getUser(): Promise<User> {
return await api
.get('/psama/user/me?hasToken')
.then((response: User) => {
user = response;
return response;
})
.catch((error) => {
console.error('Error getting user:\n', error);
throw error;
});
}
async function refreshToken() {
const newLongTermToken = await api
.get('/psama/user/me/refresh_long_term_token')
.then((response: LongTermTokenResponse) => {
return response.userLongTermToken;
})
.catch((error) => {
console.error(error);
return;
});
if (!newLongTermToken) {
refreshButtonText = 'Error';
const refreshErrorToast: ToastSettings = {
message:
'An error occured while refreshing your token. Please try again later. If this problem persists, please contact an administrator.',
background: 'variant-filled-error',
};
toastStore.trigger(refreshErrorToast);
return;
}
user.token = newLongTermToken;
refreshButtonText = 'Refreshed!';
refreshButtonDisabled = true;
rows = updateRows();
}
function revealToken() {
revealed = !revealed;
rows = updateRows();
}
function updateRows() {
if (!revealed || !user.token) return 1;
let fontSizeInPt = +window.getComputedStyle(textarea).fontSize.split('px')[0] * 0.75;
return Math.ceil((user.token.length * (fontSizeInPt - 1)) / textarea.clientWidth);
}
</script>

<div id="user-token-container">
{#await getUser()}
<ProgressRadial width="w-10" value={undefined} />
{:then}
<div id="user-token" class="card variant-filled-sureface">
<header class="card-header">PIC-SURE Token</header>
<section class="p-4 grid grid-cols-2 gap-y-2 items-center">
<label for="account">Account:</label>
<span id="account" class="w-full">{account}</span>
<label for="token">Token:</label>
<textarea id="token" bind:value={displayedToken} bind:this={textarea} {rows} disabled
></textarea>
<label for="expires">Expires:</label>
<div>
<span id="expires" class="w-1/3 mr-2"
>{new Date(expires).toString().substring(0, 24)}</span
>
{#if badge}
<span id="expires-badge" class="badge {badge}" data-testid="expires-badge"
>{badge.toUpperCase()}</span
>
{/if}
</div>
</section>
<footer class="card-footer">
<CopyButton
buttonText="Copy"
itemToCopy={user.token || ''}
classes={['variant-ringed-primary']}
/>
<button
id="refresh-button"
class="btn variant-ringed-primary"
on:click={confirmRefreshToken}
disabled={refreshButtonDisabled}>{refreshButtonText}</button
>
<button id="reveal-button" class="btn variant-ringed-primary" on:click={revealToken}
>{revealButtonText}</button
>
</footer>
</div>
{:catch}
<ErrorAlert
title="An error occured while to retrieving your account. If this problem persists, please
contact an administrator."
/>
{/await}
</div>

<style>
#user-token-container {
display: flex;
justify-content: center;
width: 52rem;
}
#user-token-container #user-token {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1rem;
}
#user-token-container #user-token section {
min-width: 50rem;
grid-template-columns: min-content auto;
}
#user-token-container #user-token section textarea {
max-width: 48rem;
word-wrap: break-word;
height: fit-content;
}
#user-token-container #user-token section label {
text-align: right;
clear: both;
float: left;
margin-right: 15px;
}
</style>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/stores';
import DataSetStore from '$lib/store/dataset';
import DataSetStore from '$lib/stores/dataset';
export let data = { cell: '', row: { archived: false } };
let toggleButton: HTMLButtonElement;
Expand Down
12 changes: 12 additions & 0 deletions src/lib/components/dataset/cell/CopyButtonCell.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script lang="ts">
import CopyButton from '$lib/components/CopyButton.svelte';
export let data = { cell: '', row: {} };
let buttonText = 'Copy';
const buttonClasses = ['w-24', 'bg-primary-500', 'text-on-primary-token'];
</script>

<div class="flex justify-between items-center">
<span class="uuid">{data.cell}</span>
<CopyButton {buttonText} itemToCopy={data.cell} classes={buttonClasses} />
</div>
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<script lang="ts">
import { DataHandler } from '@vincjo/datatables';
import ThSort from './accessories/sort.svelte';
import ThFilter from './accessories/filter.svelte';
import Search from './accessories/search.svelte';
import RowsPerPage from './accessories/rows.svelte';
import RowCount from './accessories/count.svelte';
import Pagination from './accessories/pagination.svelte';
import ThSort from './accessories/Sort.svelte';
import ThFilter from './accessories/Filter.svelte';
import Search from './accessories/Search.svelte';
import RowsPerPage from './accessories/Rows.svelte';
import RowCount from './accessories/Count.svelte';
import Pagination from './accessories/Pagination.svelte';
import type { Indexable } from '$lib/types';
interface Column {
Expand Down
Loading

0 comments on commit c926c3c

Please sign in to comment.