From 22a6278a9bc1959932d381860d7d2d36ebfc636f Mon Sep 17 00:00:00 2001
From: Xiao Yijun <xiaoyijun@silverhand.io>
Date: Tue, 15 Mar 2022 12:42:42 +0800
Subject: [PATCH] feat(console): application details settings (#381)

---
 packages/console/src/App.tsx                  |   8 +-
 .../CopyToClipboard/index.module.scss         |   1 +
 .../ApplicationDetails/index.module.scss      |  31 +++++
 .../src/pages/ApplicationDetails/index.tsx    | 130 +++++++++++++++---
 packages/phrases/src/locales/en.ts            |  13 ++
 packages/phrases/src/locales/zh-cn.ts         |  13 ++
 6 files changed, 176 insertions(+), 20 deletions(-)

diff --git a/packages/console/src/App.tsx b/packages/console/src/App.tsx
index e58d797d9..e5fa00f8a 100644
--- a/packages/console/src/App.tsx
+++ b/packages/console/src/App.tsx
@@ -1,5 +1,5 @@
 import React, { useEffect } from 'react';
-import { BrowserRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
+import { BrowserRouter, Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
 import { SWRConfig } from 'swr';
 import './scss/normalized.scss';
 
@@ -44,7 +44,11 @@ const Main = () => {
             <Routes>
               <Route path="applications">
                 <Route index element={<Applications />} />
-                <Route path=":id" element={<ApplicationDetails />} />
+                <Route path=":id">
+                  <Route index element={<Navigate to="settings" />} />
+                  <Route path="settings" element={<ApplicationDetails />} />
+                  <Route path="advanced-settings" element={<ApplicationDetails />} />
+                </Route>
               </Route>
               <Route path="api-resources">
                 <Route index element={<ApiResources />} />
diff --git a/packages/console/src/components/CopyToClipboard/index.module.scss b/packages/console/src/components/CopyToClipboard/index.module.scss
index 8dc478d86..a84cf12a4 100644
--- a/packages/console/src/components/CopyToClipboard/index.module.scss
+++ b/packages/console/src/components/CopyToClipboard/index.module.scss
@@ -12,6 +12,7 @@
   .row {
     display: flex;
     align-items: center;
+    justify-content: space-between;
     cursor: text;
 
     > svg {
diff --git a/packages/console/src/pages/ApplicationDetails/index.module.scss b/packages/console/src/pages/ApplicationDetails/index.module.scss
index 454ce5f07..78f62ca6d 100644
--- a/packages/console/src/pages/ApplicationDetails/index.module.scss
+++ b/packages/console/src/pages/ApplicationDetails/index.module.scss
@@ -59,3 +59,34 @@
     }
   }
 }
+
+.container .body {
+  > :not(:first-child) {
+    margin-top: _.unit(6);
+  }
+
+  .tabContent {
+    form {
+      >:not(:first-child) {
+        margin-top: _.unit(6);
+      }
+
+      .fields {
+        border-bottom: 1px solid var(--color-border);
+        padding-bottom: _.unit(6);
+
+        > div {
+          @include _.form-text-field;
+        }
+
+        .copy {
+          @include _.form-text-field;
+        }
+      }
+
+      .submit {
+        text-align: right;
+      }
+    }
+  }
+}
diff --git a/packages/console/src/pages/ApplicationDetails/index.tsx b/packages/console/src/pages/ApplicationDetails/index.tsx
index c78101230..d06b45304 100644
--- a/packages/console/src/pages/ApplicationDetails/index.tsx
+++ b/packages/console/src/pages/ApplicationDetails/index.tsx
@@ -1,46 +1,140 @@
 import { Application } from '@logto/schemas';
-import React from 'react';
+import React, { useEffect } from 'react';
+import { useForm } from 'react-hook-form';
 import { useTranslation } from 'react-i18next';
-import { useParams } from 'react-router-dom';
+import { useLocation, useParams } from 'react-router-dom';
 import useSWR from 'swr';
 
 import BackLink from '@/components/BackLink';
 import Button from '@/components/Button';
 import Card from '@/components/Card';
 import CopyToClipboard from '@/components/CopyToClipboard';
+import FormField from '@/components/FormField';
 import ImagePlaceholder from '@/components/ImagePlaceholder';
+import TabNav, { TabNavLink } from '@/components/TabNav';
+import TextInput from '@/components/TextInput';
 import { RequestError } from '@/swr';
 import { applicationTypeI18nKey } from '@/types/applications';
 
 import * as styles from './index.module.scss';
 
+type OidcConfig = {
+  authorization_endpoint: string;
+  userinfo_endpoint: string;
+  token_endpoint: string;
+};
+
 const ApplicationDetails = () => {
   const { id } = useParams();
+  const location = useLocation();
+
   const { data, error } = useSWR<Application, RequestError>(id && `/api/applications/${id}`);
+  const { data: oidcConfig, error: fetchOidcConfigError } = useSWR<OidcConfig, RequestError>(
+    '/oidc/.well-known/openid-configuration'
+  );
+  const isLoading = !data && !error && !fetchOidcConfigError;
+  const dataFetched = data && oidcConfig;
+
+  const { handleSubmit, register, reset } = useForm<Application>({
+    defaultValues: data,
+  });
+
   const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
-  const isLoading = !data && !error;
+
+  const isAdvancedSettings = location.pathname.includes('advanced-settings');
+
+  useEffect(() => {
+    if (!data) {
+      return;
+    }
+    reset(data);
+  }, [data, reset]);
+
+  const onSubmit = handleSubmit((formData) => {
+    console.log(formData);
+  });
 
   return (
     <div className={styles.container}>
       <BackLink to="/applications">{t('application_details.back_to_applications')}</BackLink>
       {isLoading && <div>loading</div>}
       {error && <div>{`error occurred: ${error.metadata.code}`}</div>}
-      {data && (
-        <Card className={styles.header}>
-          <ImagePlaceholder size={76} borderRadius={16} />
-          <div className={styles.metadata}>
-            <div className={styles.name}>{data.name}</div>
-            <div>
-              <div className={styles.type}>{t(`${applicationTypeI18nKey[data.type]}.title`)}</div>
-              <div className={styles.verticalBar} />
-              <div className={styles.text}>App ID</div>
-              <CopyToClipboard value={data.id} className={styles.copy} />
+      {dataFetched && (
+        <>
+          <Card className={styles.header}>
+            <ImagePlaceholder size={76} borderRadius={16} />
+            <div className={styles.metadata}>
+              <div className={styles.name}>{data.name}</div>
+              <div>
+                <div className={styles.type}>{t(`${applicationTypeI18nKey[data.type]}.title`)}</div>
+                <div className={styles.verticalBar} />
+                <div className={styles.text}>App ID</div>
+                <CopyToClipboard value={data.id} className={styles.copy} />
+              </div>
             </div>
-          </div>
-          <div>
-            <Button title="admin_console.application_details.check_help_guide" />
-          </div>
-        </Card>
+            <div>
+              <Button title="admin_console.application_details.check_help_guide" />
+            </div>
+          </Card>
+          <Card className={styles.body}>
+            <TabNav>
+              <TabNavLink href={`/applications/${data.id}/settings`}>
+                {t('application_details.settings')}
+              </TabNavLink>
+              <TabNavLink href={`/applications/${data.id}/advanced-settings`}>
+                {t('application_details.advanced_settings')}
+              </TabNavLink>
+            </TabNav>
+            <div className={styles.tabContent}>
+              <form onSubmit={onSubmit}>
+                <div className={styles.fields}>
+                  {!isAdvancedSettings && (
+                    <>
+                      <FormField
+                        isRequired
+                        title="admin_console.application_details.application_name"
+                      >
+                        <TextInput {...register('name', { required: true })} />
+                      </FormField>
+                      <FormField title="admin_console.application_details.description">
+                        <TextInput {...register('description')} />
+                      </FormField>
+                      <FormField title="admin_console.application_details.authorization_endpoint">
+                        <CopyToClipboard
+                          className={styles.copy}
+                          value={oidcConfig.authorization_endpoint}
+                        />
+                      </FormField>
+                    </>
+                  )}
+                  {isAdvancedSettings && (
+                    <>
+                      <FormField title="admin_console.application_details.token_endpoint">
+                        <CopyToClipboard
+                          className={styles.copy}
+                          value={oidcConfig.token_endpoint}
+                        />
+                      </FormField>
+                      <FormField title="admin_console.application_details.user_info_endpoint">
+                        <CopyToClipboard
+                          className={styles.copy}
+                          value={oidcConfig.userinfo_endpoint}
+                        />
+                      </FormField>
+                    </>
+                  )}
+                </div>
+                <div className={styles.submit}>
+                  <Button
+                    htmlType="submit"
+                    type="primary"
+                    title="admin_console.application_details.save_changes"
+                  />
+                </div>
+              </form>
+            </div>
+          </Card>
+        </>
       )}
     </div>
   );
diff --git a/packages/phrases/src/locales/en.ts b/packages/phrases/src/locales/en.ts
index ada5055ee..4d3bb86a1 100644
--- a/packages/phrases/src/locales/en.ts
+++ b/packages/phrases/src/locales/en.ts
@@ -80,6 +80,19 @@ const translation = {
     application_details: {
       back_to_applications: 'Back to Applications',
       check_help_guide: 'Check Help Guide',
+      settings: 'Settings',
+      advanced_settings: 'Advanced Settings',
+      application_name: 'Application Name',
+      description: 'Description',
+      authorization_endpoint: 'Authorization Endpiont',
+      redirect_uri: 'Redirect URI',
+      post_sign_out_redirect_uri: 'Post Sign Out Redirect URI',
+      add_another: 'Add another',
+      id_token_expiration: 'ID Token Expiration',
+      refresh_token_expiration: 'Refresh Token Expiration',
+      token_endpoint: 'Token Endpoint',
+      user_info_endpoint: 'User Info Endpoint',
+      save_changes: 'Save Changes',
     },
     api_resources: {
       title: 'API Resources',
diff --git a/packages/phrases/src/locales/zh-cn.ts b/packages/phrases/src/locales/zh-cn.ts
index 1c74bbd33..1fd309fd9 100644
--- a/packages/phrases/src/locales/zh-cn.ts
+++ b/packages/phrases/src/locales/zh-cn.ts
@@ -82,6 +82,19 @@ const translation = {
     application_details: {
       back_to_applications: '返回应用集',
       check_help_guide: 'Check Help Guide',
+      settings: 'Settings',
+      advanced_settings: 'Advanced Settings',
+      application_name: 'Application Name',
+      description: 'Description',
+      authorization_endpoint: 'Authorization Endpiont',
+      redirect_uri: 'Redirect URI',
+      post_sign_out_redirect_uri: 'Post Sign Out Redirect URI',
+      add_another: 'Add another',
+      id_token_expiration: 'ID Token Expiration',
+      refresh_token_expiration: 'Refresh Token Expiration',
+      token_endpoint: 'Token Endpoint',
+      user_info_endpoint: 'User Info Endpoint',
+      save_changes: 'Save Changes',
     },
     api_resources: {
       title: 'API Resources',