🏡 index : ~doyle/chartered.git

author Jordan Doyle <jordan@doyle.la> 2022-09-07 23:21:25.0 +01:00:00
committer Jordan Doyle <jordan@doyle.la> 2022-09-07 23:21:25.0 +01:00:00
commit
9e8eb2dcf5d80121a7208050318bb527838d81f6 [patch]
tree
6c61d947cdd4cf51ec7ef84b0a2c01610e9de693
parent
79104c5592656af92be9cd3794e6d47aff1f4fe8
download
9e8eb2dcf5d80121a7208050318bb527838d81f6.tar.gz

Add frontend documentation



Diff

 chartered-frontend/src/components/Icon.svelte                                          | 23 +++++++++++++++++++++++
 chartered-frontend/src/components/NavItem.svelte                                       |  5 +++++
 chartered-frontend/src/components/RelativeTime.svelte                                  |  4 ++++
 chartered-frontend/src/routes/(authed)/+layout.svelte                                  |  3 +++
 chartered-frontend/src/routes/(unauthed)/+layout.svelte                                |  3 +++
 chartered-frontend/src/routes/(authed)/organisations/+page.svelte                      |  1 +
 chartered-frontend/src/routes/(authed)/search/+page.svelte                             |  2 ++
 chartered-frontend/src/routes/(authed)/crates/[organisation]/+page.svelte              | 19 +++++++++++++++++++
 chartered-frontend/src/routes/(authed)/crates/[organisation]/AddMember.svelte          | 32 ++++++++++++++++++++++++++++++++
 chartered-frontend/src/routes/(authed)/crates/[organisation]/Member.svelte             | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 chartered-frontend/src/routes/(authed)/organisations/list/+page.svelte                 |  1 +
 chartered-frontend/src/routes/(authed)/sessions/list/+page.svelte                      | 19 ++++++++++++++-----
 chartered-frontend/src/routes/(authed)/users/[uuid]/+page.svelte                       |  2 ++
 chartered-frontend/src/routes/(unauthed)/login/oauth/+page.svelte                      |  2 ++
 chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/+page.svelte      |  9 +++++++++
 chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/Dependency.svelte | 15 +++++++++++++--
 chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/MemberTab.svelte  |  9 +++++++++
 chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/VersionTab.svelte | 15 +++++++++++++--
 18 files changed, 203 insertions(+), 13 deletions(-)

diff --git a/chartered-frontend/src/components/Icon.svelte b/chartered-frontend/src/components/Icon.svelte
index 96e3425..2bb31ee 100644
--- a/chartered-frontend/src/components/Icon.svelte
+++ a/chartered-frontend/src/components/Icon.svelte
@@ -1,12 +1,35 @@
<script type="typescript">
    import feather from 'feather-icons';
    export const directions = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'];

    /**
     * Name of the icon, can be found on https://feathericons.com/
     */
    export let name: string;

    /**
     * Compass rotation of the icon
     */
    export let direction = 'n';

    /**
     * Thickness of the icon
     */
    export let strokeWidth: string | null = null;

    /**
     * Colour of the icon
     */
    export let stroke: string | null = null;

    /**
     * Width of the icon
     */
    export let width = '1em';

    /**
     * Height of the icon
     */
    export let height = '1em';

    $: icon = feather.icons[name];
diff --git a/chartered-frontend/src/components/NavItem.svelte b/chartered-frontend/src/components/NavItem.svelte
index 8349e8c..9acf387 100644
--- a/chartered-frontend/src/components/NavItem.svelte
+++ a/chartered-frontend/src/components/NavItem.svelte
@@ -12,7 +12,12 @@
     */
    export let aliases: string[] = [];

    /**
     * Contains whether this link is currently considered active based on the current URL.
     */
    let active: boolean;

    // whenever the page name updates, check if the link (or its aliases) match up and set as active
    $: if (href === '/') {
        active = $page.url.pathname === href;
    } else {
diff --git a/chartered-frontend/src/components/RelativeTime.svelte b/chartered-frontend/src/components/RelativeTime.svelte
index 277ef77..e890b48 100644
--- a/chartered-frontend/src/components/RelativeTime.svelte
+++ a/chartered-frontend/src/components/RelativeTime.svelte
@@ -1,6 +1,10 @@
<script type="typescript">
    import { DateTime } from 'luxon';

    /**
     * A time string to convert to relative time, can be anything that can be read by
     * `Date.parse`.
     */
    export let time: string;

    const date = DateTime.fromMillis(Date.parse(time));
diff --git a/chartered-frontend/src/routes/(authed)/+layout.svelte b/chartered-frontend/src/routes/(authed)/+layout.svelte
index 56eb030..08d24f8 100644
--- a/chartered-frontend/src/routes/(authed)/+layout.svelte
+++ a/chartered-frontend/src/routes/(authed)/+layout.svelte
@@ -1,9 +1,12 @@
<script type="typescript">
    import { auth } from '../../stores/auth';
    import { goto } from '$app/navigation';
    import Nav from '../../components/Nav.svelte';
    import NavItem from '../../components/NavItem.svelte';

    // watch the `$auth` store for changes to authentication, if their `$auth` disappears
    // (such as from expiry), redirect to the login page. this also covers the case where
    // the user requests `/`, we'll redirect straight to login from this too.
    $: if (!$auth) {
        goto('/auth/login', { replaceState: true });
    }
diff --git a/chartered-frontend/src/routes/(unauthed)/+layout.svelte b/chartered-frontend/src/routes/(unauthed)/+layout.svelte
index 009c4cf..78c4b3e 100644
--- a/chartered-frontend/src/routes/(unauthed)/+layout.svelte
+++ a/chartered-frontend/src/routes/(unauthed)/+layout.svelte
@@ -1,7 +1,10 @@
<script type="typescript">
    import { auth } from '../../stores/auth';
    import { goto } from '$app/navigation';

    // watch the `$auth` store, if the user suddenly becomes authenticated (such as after login)
    // then redirect to the homepage. this also covers the case where the user navigates directly
    // to login despite already having a session
    $: if ($auth) {
        goto('/', { replaceState: true });
    }
diff --git a/chartered-frontend/src/routes/(authed)/organisations/+page.svelte b/chartered-frontend/src/routes/(authed)/organisations/+page.svelte
index 70f3774..a5e37e2 100644
--- a/chartered-frontend/src/routes/(authed)/organisations/+page.svelte
+++ a/chartered-frontend/src/routes/(authed)/organisations/+page.svelte
@@ -1,5 +1,6 @@
<script type="typescript">
    import { goto } from '$app/navigation';

    // redirect to list view
    goto('/organisations/list');
</script>
diff --git a/chartered-frontend/src/routes/(authed)/search/+page.svelte b/chartered-frontend/src/routes/(authed)/search/+page.svelte
index 7133603..3bf7d66 100644
--- a/chartered-frontend/src/routes/(authed)/search/+page.svelte
+++ a/chartered-frontend/src/routes/(authed)/search/+page.svelte
@@ -1,9 +1,11 @@
<script type="typescript">
    import { page } from '$app/stores';
    import { request } from '../../../stores/auth';
    import Spinner from '../../../components/Spinner.svelte';
    import type { Search } from '../../../types/crate';

    // whenever the `q` query parameter changes, send a request to the backend with that
    // search term.
    let searchPromise: Promise<Search>;
    $: searchPromise = request(`/web/v1/crates/search?q=${$page.url.searchParams.get('q')}`);
</script>
diff --git a/chartered-frontend/src/routes/(authed)/crates/[organisation]/+page.svelte b/chartered-frontend/src/routes/(authed)/crates/[organisation]/+page.svelte
index 7928ca9..3bfc3ae 100644
--- a/chartered-frontend/src/routes/(authed)/crates/[organisation]/+page.svelte
+++ a/chartered-frontend/src/routes/(authed)/crates/[organisation]/+page.svelte
@@ -9,9 +9,17 @@
    import AddMember from './AddMember.svelte';
    import type { CrateMembers, CrateMember } from '../../../../types/crate';

    // Load the requested organisation from the URL
    let organisationPromise: Promise<OrganisationDetail & CrateMembers>;
    $: organisationPromise = request(`/web/v1/organisations/${$page.params.organisation}`);

    /**
     * Whenever a member is updated/added/deleted to this organisation, we'll want to reload to ensure we
     * show the user exactly what the server currently sees.
     *
     * @param event a struct containing the updated member's UUID, so we can empty the newMember value if that member
     *              has just been added to we don't show them twice.
     */
    function reload(event: { detail: string }) {
        organisationPromise = request(`/web/v1/organisations/${$page.params.organisation}`);

@@ -20,11 +28,18 @@
        }
    }

    /**
     * Contains all the possible tabs, used for maintaining state on the current tab.
     */
    enum Tab {
        CRATES,
        MEMBERS,
    }

    /**
     * Mapping of `Tab`s to their human-readable form alongside a friendly icon to show to the
     * user.
     */
    const allTabs = [
        {
            id: Tab.CRATES,
@@ -38,7 +53,11 @@
        },
    ];

    // binding to the current tab the user has selected
    let currentTab = Tab.CRATES;

    // contains the member the user is currently considering adding to the org & has not yet persisted to
    // the server.
    let newMember: CrateMember | null = null;
</script>

diff --git a/chartered-frontend/src/routes/(authed)/crates/[organisation]/AddMember.svelte b/chartered-frontend/src/routes/(authed)/crates/[organisation]/AddMember.svelte
index ad6e964..0d49972 100644
--- a/chartered-frontend/src/routes/(authed)/crates/[organisation]/AddMember.svelte
+++ a/chartered-frontend/src/routes/(authed)/crates/[organisation]/AddMember.svelte
@@ -6,20 +6,46 @@
    import { createEventDispatcher } from 'svelte';
    import type { UserSearch, UserSearchUser } from '../../../../types/user';

    // Create the dispatcher to send an event whenever a new member is selected
    // by the user.
    const dispatch = createEventDispatcher();

    /**
     * A list of UUIDs to hide from the search, so we can skip showing users that
     * are already members.
     */
    export let hideUuids: string[] = [];

    /**
     * Contains whether the search results are currently loading so a spinner
     * can be shown.
     */
    let loading = false;

    /**
     * Binding to the search terms the user has entered.
     */
    let search = '';

    /**
     * A list of search results from the backend
     */
    let searchResults: UserSearchUser[] = [];

    // update `searchResults` whenever `search` is updated
    $: performSearch(search);

    // debounce the user's input for 250ms, so we don't just spam the backend with search
    // requests even though the user isn't finished yet.
    const onInput = debounce((event) => {
        search = event.target.value;
    }, 250);

    /**
     * Call the backend and fetch user results for the user's given search terms.
     *
     * @param search terms to search for
     */
    async function performSearch(search: string) {
        if (search === '') {
            return;
@@ -29,7 +55,6 @@

        try {
            let result = await request<UserSearch>(`/web/v1/users/search?q=${search}`);

            searchResults = result.users || [];
        } catch (e: unknown) {
            console.log(e);
@@ -38,6 +63,11 @@
        }
    }

    /**
     * Send an event back to the parent component whenever a user is selected.
     *
     * @param member member to send to the parent component
     */
    function dispatchNewMember(member: UserSearchUser) {
        dispatch('new', member);
        searchResults = [];
diff --git a/chartered-frontend/src/routes/(authed)/crates/[organisation]/Member.svelte b/chartered-frontend/src/routes/(authed)/crates/[organisation]/Member.svelte
index c2af427..6273faa 100644
--- a/chartered-frontend/src/routes/(authed)/crates/[organisation]/Member.svelte
+++ a/chartered-frontend/src/routes/(authed)/crates/[organisation]/Member.svelte
@@ -10,32 +10,76 @@

    const dispatch = createEventDispatcher();

    let clazz = '';

    /**
     * The name of the organisation that this is a `member` of.
     */
    export let organisation: string;

    /**
     * The name of the crate that this is a `member` of, or `null` if we're showing organisation
     * results.
     */
    export let crate: string | null = null;

    /**
     * The member to show
     */
    export let member: CrateMember;

    /**
     * A list of new permissions for the user, this is normally set by the user in the UI via bindings and default
     * to the user's current permissions. Whenever this differs from the user's current permissions then the save
     * icon is shown. This is exposed to the consumer for new ("prospective") members that don't currently exist
     * on the backend, where the consumer wants to give a default `VISIBLE` permission but also show the save icon.
     */
    export let newPermissions = member.permissions;

    /**
     * A list of possible permissions this user can be given.
     */
    export let possiblePermissions: string[];

    /**
     * A list of CSS classes to add to the outer div.
     */
    let clazz = '';
    export { clazz as class };

    /**
     * Whether the member is currently being persisted to the backend and a spinner is showing.
     */
    let saving = false;

    /**
     * Any errors that happened upon the last invocation of `save` to give feedback to the user.
     */
    let error: string | null = null;

    /**
     * Persist updates to this member to the backend.
     */
    async function save() {
        saving = true;
        error = null;

        try {
            // determine the HTTP verb to send for this membership change.
            let method;
            if (!newPermissions.includes('VISIBLE')) {
                // if the user is removing the VISIBLE permission from this member then it's a DELETE
                // operation otherwise their membership would be useless.
                method = 'DELETE';
            } else if (member.permissions.length === 0) {
                // if the member did not have initial permissions on this crate/org then they're a new
                // member to it, welcome aboard!
                method = 'PUT';
            } else {
                // anything else is simply just an update to an existing member
                method = 'PATCH';
            }

            // this component is called from both organisation views and crate views, so we need to figure
            // out which one we need to persist the changes to...
            let url;
            if (crate) {
                url = `web/v1/crates/${organisation}/${crate}`;
@@ -43,6 +87,7 @@
                url = `web/v1/organisations/${organisation}`;
            }

            // send the membership update to the backend
            let result = await fetch(`${BASE_URL}/a/${$auth?.auth_key}/${url}/members`, {
                method,
                headers: {
@@ -61,6 +106,9 @@
                throw new Error(json.error);
            }

            // fast-update the permissions locally to hide the save button, then prompt the parent
            // component to update their membership list so the user gets the most up-to-date view
            // of permissions that the server sees.
            member.permissions = newPermissions;
            dispatch('updated', member.uuid);
        } catch (e) {
diff --git a/chartered-frontend/src/routes/(authed)/organisations/list/+page.svelte b/chartered-frontend/src/routes/(authed)/organisations/list/+page.svelte
index 5a6e8d7..1754939 100644
--- a/chartered-frontend/src/routes/(authed)/organisations/list/+page.svelte
+++ a/chartered-frontend/src/routes/(authed)/organisations/list/+page.svelte
@@ -1,9 +1,10 @@
<script type="typescript">
    import Spinner from '../../../../components/Spinner.svelte';
    import { request } from '../../../../stores/auth';
    import type { OrganisationList } from '../../../../types/organisations';
    import ErrorAlert from '../../../../components/ErrorAlert.svelte';

    // fetch a list of all the current user's organisations from the backend
    const organisationsPromise = request<OrganisationList>('/web/v1/organisations');
</script>

diff --git a/chartered-frontend/src/routes/(authed)/sessions/list/+page.svelte b/chartered-frontend/src/routes/(authed)/sessions/list/+page.svelte
index 961347a..d866082 100644
--- a/chartered-frontend/src/routes/(authed)/sessions/list/+page.svelte
+++ a/chartered-frontend/src/routes/(authed)/sessions/list/+page.svelte
@@ -6,16 +6,23 @@
    import Icon from '../../../../components/Icon.svelte';
    import DeleteSessionModal from './DeleteSessionModal.svelte';

    let sessionPromise = loadSessions();

    /**
     * If not null, then the user is currently attempting to delete a session and a modal is being
     * shown for them to confirm.
     */
    let deleting: Session | null = null;

    function loadSessions(): Promise<Sessions> {
        return request<Sessions>('/web/v1/sessions');
    }
    /**
     * Grab the list of current sessions from the user from the backend.
     */
    let sessionPromise: Promise<Sessions> = request<Sessions>('/web/v1/sessions');

    /**
     * Reload all the user's current sessions whenever a session is deleted so the user gets
     * an up-to-date view of what the backend sees.
     */
    function reloadSessions() {
        sessionPromise = loadSessions();
        sessionPromise = request<Sessions>('/web/v1/sessions');
    }
</script>

diff --git a/chartered-frontend/src/routes/(authed)/users/[uuid]/+page.svelte b/chartered-frontend/src/routes/(authed)/users/[uuid]/+page.svelte
index 889c1ee..d0e39ea 100644
--- a/chartered-frontend/src/routes/(authed)/users/[uuid]/+page.svelte
+++ a/chartered-frontend/src/routes/(authed)/users/[uuid]/+page.svelte
@@ -5,6 +5,8 @@
    import Icon from '../../../../components/Icon.svelte';
    import type { User } from '../../../../types/user';

    // grab the requested user from the backend and determine a `displayName` for them to show on their
    // profile.
    let userPromise: Promise<User & { displayName?: string }>;
    $: userPromise = request<User>(`/web/v1/users/info/${$page.params.uuid}`).then(
        (user: User & { displayName?: string }) => {
diff --git a/chartered-frontend/src/routes/(unauthed)/login/oauth/+page.svelte b/chartered-frontend/src/routes/(unauthed)/login/oauth/+page.svelte
index 9758c10..c639297 100644
--- a/chartered-frontend/src/routes/(unauthed)/login/oauth/+page.svelte
+++ a/chartered-frontend/src/routes/(unauthed)/login/oauth/+page.svelte
@@ -1,9 +1,11 @@
<script type="typescript">
    import { page } from '$app/stores';
    import { handleOAuthCallback } from '../../../../stores/auth';
    import Spinner from '../../../../components/Spinner.svelte';
    import ErrorAlert from '../../../../components/ErrorAlert.svelte';

    // pass the payload onto the backend to verify and create a session, we'll just show a
    // spinner in the meantime.
    const callback = handleOAuthCallback($page.url.search);
</script>

diff --git a/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/+page.svelte b/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/+page.svelte
index 5341842..216565a 100644
--- a/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/+page.svelte
+++ a/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/+page.svelte
@@ -9,12 +9,19 @@
    import VersionTab from './VersionTab.svelte';
    import MemberTab from './MemberTab.svelte';

    /**
     * Contains all the possible tabs, used for maintaining state on the current tab.
     */
    enum Tab {
        README,
        VERSIONS,
        MEMBERS,
    }

    /**
     * Mapping of `Tab`s to their human-readable form alongside a friendly icon to show to the
     * user.
     */
    const allTabs = [
        {
            id: Tab.README,
@@ -33,9 +40,11 @@
        },
    ];

    // lookup the crate currently requested by the user based on the URL
    let cratePromise: Promise<Crate>;
    $: cratePromise = request(`/web/v1/crates/${$page.params.organisation}/${$page.params.crate}`);

    // binding to the current tab the user has selected
    let currentTab = Tab.README;
</script>

diff --git a/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/Dependency.svelte b/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/Dependency.svelte
index 0a84c5e..3569961 100644
--- a/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/Dependency.svelte
+++ a/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/Dependency.svelte
@@ -1,11 +1,22 @@
<script type="typescript">
    import type { VersionDependency } from '../../../../../types/crate';

    let clazz = '';

    /**
     * The dependency to show
     */
    export let dependency: VersionDependency;

    /**
     * CSS classes to apply to the outer div drawn by this component.
     */
    let clazz = '';
    export { clazz as class };

    /**
     * Extracts the "organisation" part of an SSH url if one exists, otherwise returns the
     * full URL. This currently assumes that all dependencies with an SSH URI are dependencies
     * local to this instance of chartered, but that's not always the case.
     */
    function getLocalDependencyOrganisation(): string | undefined {
        const s = dependency.registry?.split('/');

diff --git a/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/MemberTab.svelte b/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/MemberTab.svelte
index d1af600..9abfc2e 100644
--- a/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/MemberTab.svelte
+++ a/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/MemberTab.svelte
@@ -5,11 +5,20 @@
    import Member from '../Member.svelte';
    import type { CrateMembers, CrateMember } from '../../../../../types/crate';

    /**
     * Binding to the latest user selected after a search, this is a "prospective member" that is not yet
     * persisted to the backend until the user presses the save button.
     */
    let newMember: CrateMember | null = null;

    // Grab all the current crate's members
    let membersPromise: Promise<CrateMembers>;
    $: membersPromise = request(`/web/v1/crates/${$page.params.organisation}/${$page.params.crate}/members`);

    /**
     * When a member is updated/added/deleted to this crate, we'll want to reload to show the user exactly
     * what the server sees the current state is.
     */
    function reloadMembers() {
        newMember = null;
        membersPromise = request(`/web/v1/crates/${$page.params.organisation}/${$page.params.crate}/members`);
diff --git a/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/VersionTab.svelte b/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/VersionTab.svelte
index 761a785..a826cbe 100644
--- a/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/VersionTab.svelte
+++ a/chartered-frontend/src/routes/(authed)/crates/[organisation]/[crate]/VersionTab.svelte
@@ -1,13 +1,24 @@
<script type="typescript">
    import type { Version } from '../../../../../types/crate';
    import Icon from '../../../../../components/Icon.svelte';
    import RelativeTime from '../../../../../components/RelativeTime.svelte';

    let clazz = '';

    /**
     * The crate version to draw.
     */
    export let version: Version;

    /**
     * CSS classes to apply to the outer element.
     */
    let clazz = '';
    export { clazz as class };

    /**
     * Converts a number of bytes to a friendly human-readable form.
     *
     * @param size number of bytes
     */
    function humanFileSize(size: number): string {
        const i = Math.floor(Math.log(size) / Math.log(1024));
        return Number((size / Math.pow(1024, i)).toFixed(2)) + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];