diff --git a/i18n/en.pot b/i18n/en.pot index 633477d..f77f727 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,17 +5,23 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2024-11-21T12:50:12.197Z\n" -"PO-Revision-Date: 2024-11-21T12:50:12.198Z\n" +"POT-Creation-Date: 2024-11-30T16:31:03.946Z\n" +"PO-Revision-Date: 2024-11-30T16:31:03.947Z\n" msgid "ERROR" msgstr "ERROR" -msgid "Loading..." -msgstr "Loading..." +msgid "Namespaces" +msgstr "Namespaces" -msgid "Hello {{name}}" -msgstr "Hello {{name}}" +msgid "DataStore" +msgstr "DataStore" -msgid "Welcome to DHIS2 with TypeScript!" -msgstr "Welcome to DHIS2 with TypeScript!" +msgid "User DataStore" +msgstr "User DataStore" + +msgid "Search" +msgstr "Search" + +msgid "Namespace" +msgstr "Namespace" diff --git a/package.json b/package.json index 3ad8b0b..e557479 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,11 @@ "@types/jest": "^29.5.14", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", + "react-router-dom": "^7.0.1", "typescript": "^5" }, "dependencies": { - "@dhis2/app-runtime": "^3.11.3" + "@dhis2/app-runtime": "^3.11.3", + "prop-types": "^15.8.1" } } diff --git a/src/App.module.css b/src/App.module.css index a824cff..e219a95 100644 --- a/src/App.module.css +++ b/src/App.module.css @@ -2,8 +2,78 @@ width: 100%; height: 100%; display: flex; - flex-direction: column; - align-items: center; - justify-content: center; + flex-direction: row; font-size: 1rem; } + +.sidebar { + width: 20%; + margin: 0.1em; +} + +.sidebarContent { + padding: 0.3em; + width: 20%; +} + +.sidebarList { + margin-top: 1em; +} + +.sidebarList ul { + list-style-type: 'none'; + margin: 0; + padding: 0; +} + +.top { + margin-top: 0.5em; +} + +.bottom { + margin-bottom: 0.5em; +} + +.main { + width: 80%; + margin: 0.1em; +} + +.keysTable { + margin-top: 10px; + padding: 0.2em; +} + +/* sourced and adapted from https://github.com/dhis2/design-specs/blob/b65e6518dcc7c16733379cd80688e67a422fc742/src/components/sidenav.css#L73 (-> 104) */ + +.navLink a { + display: block; + min-height: 36px; + padding: 10px; + /* background: var(--colors-grey100); */ + text-decoration: none; + color: var(--colors-grey900); + font-size: 15px; + display: flex; + align-items: center; +} +.navLink:hover, +.navLink a:hover { + background: var(--colors-grey300); +} +.navLink:focus, +.navLink a:focus { + outline: 2px solid white; + background: var(--colors-grey200); + outline-offset: -2px; +} + +.navLink a.active, +.navLink :global(.active) { + font-weight: 500; + background: var(--colors-grey200); + box-shadow: inset 6px 0px 0px 0px var(--colors-grey500); +} +.navLink.active:hover { + background: var(--colors-grey300); +} diff --git a/src/App.tsx b/src/App.tsx index 430298d..aa18058 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,37 +1,14 @@ -import { useDataQuery } from '@dhis2/app-runtime' -import i18n from '@dhis2/d2-i18n' import React, { FC } from 'react' -import classes from './App.module.css' - -interface QueryResults { - me: { - name: string - } -} - -const query = { - me: { - resource: 'me', - }, -} - -const MyApp: FC = () => { - const { error, loading, data } = useDataQuery(query) - - if (error) { - return {i18n.t('ERROR')} - } - - if (loading) { - return {i18n.t('Loading...')} - } +import { RouterProvider } from 'react-router-dom' +import AppWrapper from './components/appWrapper' +import { router } from './routes/router' +const App: FC = () => { return ( -
-

{i18n.t('Hello {{name}}', { name: data?.me?.name })}

-

{i18n.t('Welcome to DHIS2 with TypeScript!')}

-
+ + + ) } -export default MyApp +export default App diff --git a/src/components/EmptyArea.tsx b/src/components/EmptyArea.tsx new file mode 100644 index 0000000..3406a18 --- /dev/null +++ b/src/components/EmptyArea.tsx @@ -0,0 +1,22 @@ +import { Center } from '@dhis2/ui' +import React from 'react' +import { useParams } from 'react-router-dom' + +const EmptyArea = () => { + const { store, namespace } = useParams() + return ( + <> + {!store && ( +
+

Select a datastore to show namespaces

+
+ )} + {store && !namespace && ( +
+

Click a namespace to show keys

+
+ )} + + ) +} +export default EmptyArea diff --git a/src/components/Loader.tsx b/src/components/Loader.tsx new file mode 100644 index 0000000..7f8a2f8 --- /dev/null +++ b/src/components/Loader.tsx @@ -0,0 +1,17 @@ +import { CircularLoader } from '@dhis2/ui' +import React from 'react' + +const CenteredLoader = () => { + return ( +
+ +
+ ) +} + +export default CenteredLoader diff --git a/src/components/appWrapper.tsx b/src/components/appWrapper.tsx new file mode 100644 index 0000000..96486e6 --- /dev/null +++ b/src/components/appWrapper.tsx @@ -0,0 +1,19 @@ +import { CssReset, CssVariables } from '@dhis2/ui' +import PropTypes from 'prop-types' +import React from 'react' + +function AppWrapper({ children }) { + return ( + <> + + + {children} + + ) +} + +AppWrapper.propTypes = { + children: PropTypes.node, +} + +export default AppWrapper diff --git a/src/components/keys/keysTable.tsx b/src/components/keys/keysTable.tsx new file mode 100644 index 0000000..0f6266d --- /dev/null +++ b/src/components/keys/keysTable.tsx @@ -0,0 +1,77 @@ +import { useDataQuery } from '@dhis2/app-runtime' +import { + DataTable, + DataTableCell, + DataTableColumnHeader, + DataTableRow, + TableBody, + TableHead, +} from '@dhis2/ui' +import React, { useEffect } from 'react' +import { useParams } from 'react-router-dom' +import classes from '../../App.module.css' +import CenteredLoader from '../Loader' + +interface QueryResults { + results: [] +} + +const useNameSpaceQuery = ({ store, namespace }) => { + return useDataQuery( + { + results: { + resource: `${store}`, + id: ({ id }) => id, + }, + }, + { + variables: { + id: namespace, + }, + } + ) +} + +const KeysTable = () => { + const { store, namespace } = useParams() + const { data, loading, refetch } = useNameSpaceQuery({ store, namespace }) + + useEffect(() => { + refetch({ id: namespace }) + }, [namespace]) + + if (loading) { + return + } + + return ( +
+ + + + Keys + Actions + + + + {data?.results?.length && ( + <> + {data.results.map((key, index) => ( + + + {key} + + + Edit, Delete + + + ))} + + )} + + +
+ ) +} + +export default KeysTable diff --git a/src/components/namespaces/DataStoreLinks.tsx b/src/components/namespaces/DataStoreLinks.tsx new file mode 100644 index 0000000..e84b448 --- /dev/null +++ b/src/components/namespaces/DataStoreLinks.tsx @@ -0,0 +1,28 @@ +import { useDataQuery } from '@dhis2/app-runtime' +import PropTypes from 'prop-types' +import React from 'react' +import LinksList from './LinksList' + +interface QueryResults { + results: [] +} + +const dataStoreQuery = { + results: { + resource: 'dataStore', + }, +} + +function DataStoreLinks({ store }) { + const { error, loading, data } = useDataQuery(dataStoreQuery) + + return ( + + ) +} + +DataStoreLinks.propTypes = { + store: PropTypes.string, +} + +export default DataStoreLinks diff --git a/src/components/namespaces/LinksList.tsx b/src/components/namespaces/LinksList.tsx new file mode 100644 index 0000000..ec055e4 --- /dev/null +++ b/src/components/namespaces/LinksList.tsx @@ -0,0 +1,40 @@ +import PropTypes from 'prop-types' +import React from 'react' +import classes from '../../App.module.css' +import i18n from '../../locales' +import CenteredLoader from '../Loader' +import SidebarNavLink from '../sidebar/SidebarNavLink' + +function LinksList({ data, error, loading, store }) { + return ( +
+ {error && {i18n.t('ERROR')}} + {loading && } + {data && ( + <> +

{i18n.t('Namespaces')}

+
    + {data.results.map((namespace: string, index) => { + return ( + + ) + })} +
+ + )} +
+ ) +} + +LinksList.propTypes = { + data: PropTypes.object, + error: PropTypes.any, + loading: PropTypes.any, + store: PropTypes.string, +} + +export default LinksList diff --git a/src/components/namespaces/NamespacesLinks.tsx b/src/components/namespaces/NamespacesLinks.tsx new file mode 100644 index 0000000..a8aed92 --- /dev/null +++ b/src/components/namespaces/NamespacesLinks.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import { useParams } from 'react-router-dom' +import DataStoreLinks from './DataStoreLinks' +import UserDataStoreLinks from './UserDataStoreLinks' + +const NameSpaceLinks = () => { + const { store } = useParams() + + if (store === 'userDataStore') { + return + } + + if (store === 'dataStore') { + return + } +} + +export default NameSpaceLinks diff --git a/src/components/namespaces/UserDataStoreLinks.tsx b/src/components/namespaces/UserDataStoreLinks.tsx new file mode 100644 index 0000000..c6b0fdd --- /dev/null +++ b/src/components/namespaces/UserDataStoreLinks.tsx @@ -0,0 +1,29 @@ +import { useDataQuery } from '@dhis2/app-runtime' +import PropTypes from 'prop-types' +import React from 'react' +import LinksList from './LinksList' + +interface QueryResults { + results: [] +} + +const userDataStoreQuery = { + results: { + resource: 'userDataStore', + }, +} + +function UserDataStoreLinks({ store }) { + const { error, loading, data } = + useDataQuery(userDataStoreQuery) + + return ( + + ) +} + +UserDataStoreLinks.propTypes = { + store: PropTypes.string, +} + +export default UserDataStoreLinks diff --git a/src/components/sidebar/DataStoreSelect.tsx b/src/components/sidebar/DataStoreSelect.tsx new file mode 100644 index 0000000..5fa46da --- /dev/null +++ b/src/components/sidebar/DataStoreSelect.tsx @@ -0,0 +1,33 @@ +import { SingleSelectField, SingleSelectOption } from '@dhis2/ui' +import PropTypes from 'prop-types' +import React from 'react' +import classes from '../../App.module.css' +import i18n from '../../locales' + +const DataStoreSelect = ({ option, handleChange }) => { + return ( +
+ + + + +
+ ) +} + +DataStoreSelect.propTypes = { + handleChange: PropTypes.func, + option: PropTypes.string, +} + +export default DataStoreSelect diff --git a/src/components/sidebar/SidebarNavLink.tsx b/src/components/sidebar/SidebarNavLink.tsx new file mode 100644 index 0000000..f6ccb3c --- /dev/null +++ b/src/components/sidebar/SidebarNavLink.tsx @@ -0,0 +1,26 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { NavLink } from 'react-router-dom' +import classes from '../../App.module.css' + +const SidebarNavLink = ({ to, label }) => { + return ( +
  • + + isActive ? 'active' : isPending ? 'pending' : '' + } + > + {label} + +
  • + ) +} + +SidebarNavLink.propTypes = { + label: PropTypes.string, + to: PropTypes.string, +} + +export default SidebarNavLink diff --git a/src/components/sidebar/searchField.tsx b/src/components/sidebar/searchField.tsx new file mode 100644 index 0000000..c2a0cf4 --- /dev/null +++ b/src/components/sidebar/searchField.tsx @@ -0,0 +1,18 @@ +import { InputField } from '@dhis2/ui' +import React from 'react' +import classes from '../../App.module.css' +import i18n from '../../locales' + +const SearchField = () => { + return ( +
    + +
    + ) +} + +export default SearchField diff --git a/src/components/sidebar/sidebar.tsx b/src/components/sidebar/sidebar.tsx new file mode 100644 index 0000000..0c2732f --- /dev/null +++ b/src/components/sidebar/sidebar.tsx @@ -0,0 +1,35 @@ +import { Card, Divider } from '@dhis2/ui' +import React, { useState } from 'react' +import { useNavigate, useParams } from 'react-router-dom' +import classes from '../../App.module.css' +import NameSpaceLinks from '../namespaces/NamespacesLinks' +import DataStoreSelect from './DataStoreSelect' +import SearchField from './searchField' + +const Sidebar = () => { + const navigate = useNavigate() + const { store } = useParams() + const [option, setOption] = useState(store) + + const handleDataStoreSelect = ({ selected }) => { + setOption(selected) + navigate(`/${selected}`) + } + return ( + + + + {store && ( + <> + + + + )} + + ) +} + +export default Sidebar diff --git a/src/pages/errorPage.tsx b/src/pages/errorPage.tsx new file mode 100644 index 0000000..62f4064 --- /dev/null +++ b/src/pages/errorPage.tsx @@ -0,0 +1,27 @@ +import { Center } from '@dhis2/ui' +import React from 'react' +import { useRouteError } from 'react-router-dom' + +interface Error { + status?: number + statusText?: string + internal?: boolean + data?: string + message?: string +} + +export default function ErrorPage() { + const error: Error = useRouteError() + + return ( +
    +
    +

    Oops!

    +

    Sorry, an unexpected error has occurred

    +

    + {error.statusText || error.message} +

    +
    +
    + ) +} diff --git a/src/routes/layout.tsx b/src/routes/layout.tsx new file mode 100644 index 0000000..534ff97 --- /dev/null +++ b/src/routes/layout.tsx @@ -0,0 +1,24 @@ +import { Card } from '@dhis2/ui' +import React from 'react' +import { Outlet } from 'react-router-dom' +import classes from '../App.module.css' +import EmptyArea from '../components/EmptyArea' +import Sidebar from '../components/sidebar/sidebar' + +function Layout() { + return ( +
    + +
    + + + + +
    +
    + ) +} + +export default Layout diff --git a/src/routes/router.tsx b/src/routes/router.tsx new file mode 100644 index 0000000..bf5d2e9 --- /dev/null +++ b/src/routes/router.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import { createHashRouter } from 'react-router-dom' +import KeysTable from '../components/keys/keysTable' +import ErrorPage from '../pages/errorPage' +import Layout from './layout' + +export const router = createHashRouter([ + { + path: '/', + errorElement: , + element: , + children: [ + { + path: ':store', + children: [ + { + path: ':namespace', + element: , + }, + ], + }, + ], + }, +]) diff --git a/yarn.lock b/yarn.lock index 2c13786..9ff8dbb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2872,6 +2872,11 @@ dependencies: "@types/node" "*" +"@types/cookie@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5" + integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA== + "@types/eslint-scope@^3.7.7": version "3.7.7" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" @@ -4587,6 +4592,11 @@ cookie@0.7.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== +cookie@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.0.2.tgz#27360701532116bd3f1f9416929d176afe1e4610" + integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA== + core-js-compat@^3.38.0, core-js-compat@^3.38.1: version "3.39.0" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.39.0.tgz#b12dccb495f2601dc860bdbe7b4e3ffa8ba63f61" @@ -9563,6 +9573,23 @@ react-refresh@^0.14.2: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9" integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA== +react-router-dom@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.0.1.tgz#b1438100800313e1b4c48da0c5fdb498f81c7f96" + integrity sha512-duBzwAAiIabhFPZfDjcYpJ+f08TMbPMETgq254GWne2NW1ZwRHhZLj7tpSp8KGb7JvZzlLcjGUnqLxpZQVEPng== + dependencies: + react-router "7.0.1" + +react-router@7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.0.1.tgz#a171e35a5c6463f76b23353c4ce57b53c8b7b1b9" + integrity sha512-WVAhv9oWCNsja5AkK6KLpXJDSJCQizOIyOd4vvB/+eHGbYx5vkhcmcmwWjQ9yqkRClogi+xjEg9fNEOd5EX/tw== + dependencies: + "@types/cookie" "^0.6.0" + cookie "^1.0.1" + set-cookie-parser "^2.6.0" + turbo-stream "2.4.0" + react@^18: version "18.3.1" resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" @@ -10137,6 +10164,11 @@ set-blocking@^2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +set-cookie-parser@^2.6.0: + version "2.7.1" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz#3016f150072202dfbe90fadee053573cc89d2943" + integrity sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ== + set-function-length@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" @@ -11037,6 +11069,11 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +turbo-stream@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/turbo-stream/-/turbo-stream-2.4.0.tgz#1e4fca6725e90fa14ac4adb782f2d3759a5695f0" + integrity sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g== + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"