Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions static/js/publisher/components/PrimaryNav/PrimaryNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { brandIdState, brandStoreState } from "../../state/brandStoreState";
import { publisherState } from "../../state/publisherState";
import { validationSetsState } from "../../state/validationSetsState";
import StoreSelector from "../StoreSelector";
import { accountKeysState } from "../../state/accountKeysState";

function PrimaryNav(): React.JSX.Element {
const location = useLocation();
Expand All @@ -18,6 +19,7 @@ function PrimaryNav(): React.JSX.Element {
const publisher = useAtomValue(publisherState);
const brandId = useAtomValue(brandIdState);
const validationSets = useAtomValue(validationSetsState);
const accountKeys = useAtomValue(accountKeysState);

return (
<>
Expand Down Expand Up @@ -58,6 +60,21 @@ function PrimaryNav(): React.JSX.Element {
],
}
: null,
accountKeys && accountKeys.length > 0
? {
items: [
{
label: "My keys",
component: NavLink,
to: "/admin/account-keys",
icon: "private-key",
"aria-current": location.pathname.includes(
"/admin/account-keys",
),
},
],
}
: null,
publisher?.has_stores
? {
items: [
Expand Down
2 changes: 2 additions & 0 deletions static/js/publisher/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import usePublisher from "./usePublisher";
import useFetchAccountSnaps from "./useFetchAccountSnaps";
import useFetchPublishedSnapMetrics from "./useFetchPublishedSnapMetrics";
import useSortTableData from "./useSortTableData";
import useAccountKeys from "./useAccountKeys";

export {
useValidationSets,
Expand All @@ -32,4 +33,5 @@ export {
useFetchAccountSnaps,
useFetchPublishedSnapMetrics,
useSortTableData,
useAccountKeys,
};
17 changes: 17 additions & 0 deletions static/js/publisher/hooks/useAccountKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useQuery } from "react-query";
import { AccountKeyData } from "../types/accountKeysTypes";

function useAccountKeys() {
return useQuery("account_keys", async () => {
const response = await fetch("/account-keys.json");

if (!response.ok) {
throw new Error(response.statusText);
}

const keys = await response.json();
return keys as AccountKeyData[];
});
}

export default useAccountKeys;
15 changes: 14 additions & 1 deletion static/js/publisher/hooks/useSideNavigationData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@ import { useSetAtom } from "jotai";
import { useEffect } from "react";
import { useParams } from "react-router-dom";

import { useBrand, useBrandStores, usePublisher, useValidationSets } from "./";
import {
useAccountKeys,
useBrand,
useBrandStores,
usePublisher,
useValidationSets,
} from "./";
import { brandIdState, brandStoresState } from "../state/brandStoreState";
import { publisherState } from "../state/publisherState";
import { validationSetsState } from "../state/validationSetsState";
import { accountKeysState } from "../state/accountKeysState";

/**
* Load all the data that is needed for side navigation, more specifically:
Expand All @@ -22,11 +29,13 @@ function useSideNavigationData() {
const { data: validationSetsData } = useValidationSets();
const { data: brandStoresData } = useBrandStores();
const { data: brandData } = useBrand(storeId);
const { data: accountKeysData } = useAccountKeys();

const setBrandStores = useSetAtom(brandStoresState);
const setPublisher = useSetAtom(publisherState);
const setBrandId = useSetAtom(brandIdState);
const setValidationSets = useSetAtom(validationSetsState);
const setAccountKeys = useSetAtom(accountKeysState);

useEffect(() => {
setBrandId(brandData?.["account-id"] || brandIdState.init);
Expand All @@ -43,6 +52,10 @@ function useSideNavigationData() {
useEffect(() => {
setValidationSets(validationSetsData || validationSetsState.init);
}, [validationSetsData]);

useEffect(() => {
setAccountKeys(accountKeysData || accountKeysState.init);
}, [accountKeysData]);
}

export default useSideNavigationData;
2 changes: 2 additions & 0 deletions static/js/publisher/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import SigningKeys from "./pages/SigningKeys";
import Snaps from "./pages/Snaps";
import ValidationSet from "./pages/ValidationSet";
import ValidationSets from "./pages/ValidationSets";
import AccountKeys from "./pages/AccountKeys";

Sentry.init({
dsn: window.SENTRY_DSN,
Expand Down Expand Up @@ -89,6 +90,7 @@ root.render(
/>

<Route path="admin/account" element={<AccountDetails />} />
<Route path="admin/account-keys" element={<AccountKeys />} />

<Route path="admin/:id" element={<BrandStoreRoute />}>
<Route index element={<Navigate to="snaps" />} />
Expand Down
13 changes: 13 additions & 0 deletions static/js/publisher/pages/AccountKeys/AccountKeyError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Notification } from "@canonical/react-components";

function AccountKeysError(): React.JSX.Element {
return (
<div className="u-fixed-width">
<Notification severity="negative" title="Can't fetch account keys">
Something went wrong. Please try again later.
</Notification>
</div>
);
}

export default AccountKeysError;
41 changes: 41 additions & 0 deletions static/js/publisher/pages/AccountKeys/AccountKeySearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Button, Icon } from "@canonical/react-components";

function AccountKeysSearch(props: {
value?: string;
onChange: (v: string) => void;
}): React.JSX.Element {
return (
<div className="p-search-box">
<label className="u-off-screen" htmlFor="search">
Search account keys
</label>
<input
required
type="search"
id="search"
name="search"
className="p-search-box__input"
placeholder="Search account keys"
autoComplete="off"
value={props.value}
onChange={(e) => {
props.onChange(e.target.value);
}}
/>
<Button
type="reset"
className="p-search-box__reset"
onClick={() => {
props.onChange("");
}}
>
<Icon name="close">Clear filter</Icon>
</Button>
<Button type="submit" className="p-search-box__button">
<Icon name="search">Search</Icon>
</Button>
</div>
);
}

export default AccountKeysSearch;
62 changes: 62 additions & 0 deletions static/js/publisher/pages/AccountKeys/AccountKeys.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Col, Row, Spinner } from "@canonical/react-components";
import { useMemo, useState } from "react";

import { useAccountKeys } from "../../hooks";
import AccountKeysError from "./AccountKeyError";
import AccountKeysSearch from "./AccountKeySearch";
import AccountKeysTable from "./AccountKeysTable";

function AccountKeys(): React.JSX.Element {
const { data, isLoading, isError } = useAccountKeys();
const hasKeys = !isLoading && !isError && !!data?.length;
const [searchName, setSearchName] = useState<string>("");

const hasConstraints = useMemo(() => {
return (data ?? []).some((k) => !!k.constraints?.length);
}, [data]);

const filteredData = useMemo(() => {
if (!searchName) return data ?? [];

return (data ?? []).filter((k) => k.name.includes(searchName));
}, [data, searchName]);

return (
<>
<Row>
<Col size={12}>
<h2 className="p-heading--4">Account keys</h2>
</Col>
</Row>

{hasKeys && (
<div className="u-fixed-width">
<Row>
<Col size={6}>
<AccountKeysSearch value={searchName} onChange={setSearchName} />
</Col>
</Row>

<Row style={{ overflow: "auto" }}>
<AccountKeysTable
keys={filteredData}
hasConstraints={hasConstraints}
/>
</Row>
</div>
)}

{isLoading && <Spinner text="Loading..." />}

{isError && <AccountKeysError />}

{!isLoading && !isError && !hasKeys && (
<div className="u-fixed-width">
There are no keys associated to your account
</div>
)}
</>
);
}

export default AccountKeys;
Loading