Repository: api-ghost/api-ghost-ui
Pull request #16: feat(dashboard): Add Toggle Functionality and Component Separation for Dashboard Table
Author: login: haerim-kweon
Labels: name: enhancement
description: New feature or request
name: 0.0.1-BETA
Created: 2025-05-01T11:50:56Z | Merged: 2025-05-02T01:06:42Z



### Related Issue

- #15 

### Description

This PR introduces a visual improvement to the Dashboard table by replacing status emojis with a new `LightIndicator` component.

![toggle](https://github.com/user-attachments/assets/03c592f6-7027-4db2-88e8-cd22b3708f60)

#### Key Changes:
- Added `LightIndicator` component and its SCSS styles
- Replaced status emojis in `TableRow` with `LightIndicator`
- Updated import path for `LightIndicator` after refactoring
- Added toggle functionality for expanding detail rows in the table


### Testing

- Manually verified the visual display of the `LightIndicator` in the dashboard table
- Confirmed that toggling detail rows works as expected
- No regression in existing table behavior

### Additional Notes

- Design consistency improved by using a reusable component (`LightIndicator`)
- Ensures future scalability if the indicator logic needs expansion
- No breaking changes introduced


### Pre-Submission Checklist

- [x] Code style checks completed locally
- [x] Verified visual changes in browser
- [ ] CI pipeline results pending (will check after PR submission)



Diff:
diff --git a/src/components/LightIndicator.module.scss b/src/components/LightIndicator.module.scss
new file mode 100644
index 0000000..b9c5842
--- /dev/null
+++ b/src/components/LightIndicator.module.scss
@@ -0,0 +1,14 @@
+.indicator {
+  display: inline-block;
+  border-radius: 50%;
+}
+
+.success {
+  background: radial-gradient(circle, $gray-100 0%, $green-300 100%);
+  box-shadow: 0 0 10px $green-300;
+}
+
+.failure {
+  background: radial-gradient(circle, $red-100 0%, $red-300 100%);
+  box-shadow: 0 0 10px $red-300;
+}
diff --git a/src/components/LightIndicator.tsx b/src/components/LightIndicator.tsx
new file mode 100644
index 0000000..2f69092
--- /dev/null
+++ b/src/components/LightIndicator.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import styles from './LightIndicator.module.scss';
+
+interface LightIndicatorProps {
+  isSuccess: boolean;
+  size?: number;
+  colorSuccess?: string;
+  colorFailure?: string;
+}
+
+const LightIndicator: React.FC<LightIndicatorProps> = ({
+  isSuccess,
+  size = 8,
+  colorSuccess = '#4CAF50',
+  colorFailure = '#F44336',
+}) => {
+  const circleRadius = size / 2;
+
+  const indicatorClass = isSuccess ? styles.success : styles.failure;
+
+  return (
+    <svg
+      className={`${styles.indicator} ${indicatorClass}`}
+      width={size}
+      height={size}
+      viewBox="0 0 16 16"
+      xmlns="http://www.w3.org/2000/svg"
+    >
+      <circle cx="8" cy="8" r={circleRadius} fill={isSuccess ? colorSuccess : colorFailure} />
+    </svg>
+  );
+};
+
+export default LightIndicator;
diff --git a/src/pages/dashboard/Dashboard.tsx b/src/pages/dashboard/Dashboard.tsx
index 1de04e1..4614f83 100644
--- a/src/pages/dashboard/Dashboard.tsx
+++ b/src/pages/dashboard/Dashboard.tsx
@@ -9,7 +9,7 @@
  * The component handles tab management (selecting, adding, and closing tabs) and history item selection.
  */
 
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
 import styles from './Dashboard.module.scss';
 import Sidebar from '@/pages/dashboard/sidebar/Sidebar.tsx';
 import MainContent from '@/pages/dashboard/main-content/MainContent.tsx';
@@ -58,6 +58,15 @@ const Dashboard: React.FC = () => {
     }
   };
 
+  useEffect(() => {
+    if (selectedTab) {
+      const matched = mockHistoryList.find(h => h.id === selectedTab.id);
+      if (matched) setSelectedHistory(matched);
+    } else {
+      setSelectedHistory(null);
+    }
+  }, [selectedTab]);
+
   return (
     <div className={styles.container}>
       <div className={styles.sidebar}>
diff --git a/src/pages/dashboard/__mocks__/mockHistoryList.ts b/src/pages/dashboard/__mocks__/mockHistoryList.ts
index f28f900..851e072 100644
--- a/src/pages/dashboard/__mocks__/mockHistoryList.ts
+++ b/src/pages/dashboard/__mocks__/mockHistoryList.ts
@@ -83,7 +83,7 @@ export const mockHistoryList: HistoryItem[] = [
     ],
   },
   {
-    id: 'SC-004',
+    id: 'SC-014',
     title: 'Product Query Scenario',
     description: 'Checks product list and detail view',
     executedAt: '2025-04-24T11:30:00+09:00',
@@ -122,7 +122,7 @@ export const mockHistoryList: HistoryItem[] = [
     ],
   },
   {
-    id: 'SC-005',
+    id: 'SC-015',
     title: 'Order Failure Scenario',
     description: 'Order failure due to out-of-stock product',
     executedAt: '2025-04-25T09:10:00+09:00',
@@ -148,7 +148,7 @@ export const mockHistoryList: HistoryItem[] = [
     ],
   },
   {
-    id: 'SC-006',
+    id: 'SC-016',
     title: 'Password Reset',
     description: 'Verifies the password reset process',
     executedAt: '2025-04-26T12:45:00+09:00',
@@ -187,7 +187,7 @@ export const mockHistoryList: HistoryItem[] = [
     ],
   },
   {
-    id: 'SC-003',
+    id: 'SC-013',
     title: 'Complex User Flow Scenario',
     description: 'Verifies the entire flow: Sign Up → Login → Retrieve/Update Info → Logout, etc.',
     executedAt: '2025-04-29T10:00:00+09:00',
diff --git a/src/pages/dashboard/table-area/DetailRow.tsx b/src/pages/dashboard/table-area/DetailRow.tsx
new file mode 100644
index 0000000..0d9effb
--- /dev/null
+++ b/src/pages/dashboard/table-area/DetailRow.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import styles from './TableArea.module.scss';
+import { ResultItem } from '@/types/index.ts';
+
+interface DetailRowProps {
+  isOpen: boolean;
+  item: ResultItem;
+}
+
+const DetailRow: React.FC<DetailRowProps> = ({ isOpen, item }) => {
+  return (
+    <tr className={`${styles.detailRow} ${isOpen ? styles.expanded : ''}`}>
+      <td colSpan={5} style={{ padding: 0, border: 'none' }}>
+        <div className={`${styles.detailContentWrapper} ${isOpen ? styles.expanded : ''}`}>
+          <div className={styles.detailContent}>
+            <div className={styles.detailColumn}>
+              <h3>Start Time</h3>
+              <pre>{item.startTime}</pre>
+            </div>
+
+            <div className={styles.detailColumn}>
+              <h3>End Time</h3>
+              <pre>{item.endTime}</pre>
+            </div>
+
+            <div className={styles.detailColumn}>
+              <h3>Request Headers</h3>
+              <pre>{JSON.stringify(item.requestHeader || item.requestHeaders, null, 2)}</pre>
+            </div>
+
+            <div className={styles.detailColumn}>
+              <h3>Response Headers</h3>
+              <pre>{JSON.stringify(item.responseHeaders, null, 2)}</pre>
+            </div>
+
+            <div className={styles.detailColumn}>
+              <h3>Request Body</h3>
+              <pre>{JSON.stringify(item.requestBody, null, 2)}</pre>
+            </div>
+
+            <div className={styles.detailColumn}>
+              <h3>Response Body</h3>
+              <pre>{JSON.stringify(item.responseBody, null, 2)}</pre>
+            </div>
+          </div>
+        </div>
+      </td>
+    </tr>
+  );
+};
+
+export default DetailRow;
diff --git a/src/pages/dashboard/table-area/TableArea.module.scss b/src/pages/dashboard/table-area/TableArea.module.scss
index 6ca5651..e4466d1 100644
--- a/src/pages/dashboard/table-area/TableArea.module.scss
+++ b/src/pages/dashboard/table-area/TableArea.module.scss
@@ -13,6 +13,7 @@
   table {
     width: 100%;
     border-collapse: collapse;
+    table-layout: fixed;
 
     th,
     td {
@@ -20,25 +21,36 @@
       border: 1px solid $primary-100;
       text-align: center;
       vertical-align: top;
+      font-size: $font-size-xs;
+      word-wrap: break-word;
+      overflow: hidden;
     }
 
     th {
-      font-size: $font-size-xs;
       font-weight: $font-weight-semibold;
       white-space: nowrap;
       background-color: $primary-50;
     }
 
     td {
-      font-size: $font-size-xs;
-      white-space: pre-wrap;
-      word-break: break-word;
       background-color: $white;
       color: $gray-900;
 
+      &:nth-child(1),
+      &:nth-child(2) {
+        width: 100px;
+      }
+
+      &:nth-child(3),
+      &:nth-child(4),
+      &:nth-child(5) {
+        width: 150px;
+      }
+
       pre {
         margin: 0;
         white-space: pre-wrap;
+        word-break: break-word;
         font-family: inherit;
       }
     }
@@ -48,3 +60,58 @@
     }
   }
 }
+
+.summaryRow {
+  cursor: pointer;
+  transition: background-color 0.2s;
+
+  &:hover {
+    background-color: $primary-50;
+  }
+}
+
+.detailContentWrapper {
+  max-height: 0;
+  overflow: hidden;
+  transition: max-height 0.8s ease;
+  background-color: $white;
+  text-align: left;
+
+  &.expanded {
+    max-height: 1000px;
+  }
+}
+
+.detailContent {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 2rem;
+  padding: 1rem;
+  border-top: 1px solid $primary-100;
+  background-color: $white;
+
+  @media (max-width: 768px) {
+    grid-template-columns: 1fr;
+  }
+}
+
+.detailColumn {
+  display: flex;
+  flex-direction: column;
+
+  h3 {
+    margin: 0 0 0.5rem;
+  }
+
+  pre {
+    margin: 0;
+    background-color: $gray-50;
+    padding: 0.5rem;
+    border-radius: 4px;
+    max-height: 200px;
+    overflow: auto;
+    white-space: pre-wrap;
+    word-break: break-word;
+    font-family: inherit;
+  }
+}
diff --git a/src/pages/dashboard/table-area/TableArea.tsx b/src/pages/dashboard/table-area/TableArea.tsx
index 2f3efd6..31ce5e4 100644
--- a/src/pages/dashboard/table-area/TableArea.tsx
+++ b/src/pages/dashboard/table-area/TableArea.tsx
@@ -1,18 +1,7 @@
-/**
- * @fileoverview TableArea component displays a raw data table for request and response history.
- *
- * This component renders a table based on mock API history data.
- * It includes details such as HTTP method, endpoint, status code, duration,
- * timestamps, and headers/bodies of the requests and responses.
- *
- * Data is currently sourced from a mock object (`mockHistoryList`).
- *
- * @component
- * @returns {JSX.Element} A React functional component rendering request/response data in tabular form.
- */
-import styles from './TableArea.module.scss';
 import React from 'react';
+import styles from './TableArea.module.scss';
 import { ResultItem } from '@/types/index.ts';
+import TableRow from './TableRow.tsx';
 
 interface TableAreaProps {
   results: ResultItem[];
@@ -26,42 +15,16 @@ const TableArea: React.FC<TableAreaProps> = ({ results }) => {
         <table>
           <thead>
             <tr>
+              <th>Success</th>
               <th>Method</th>
               <th>Endpoint</th>
               <th>Status Code</th>
               <th>Duration (ms)</th>
-              <th>Start Time</th>
-              <th>End Time</th>
-              <th>Success</th>
-              <th>Request Header</th>
-              <th>Request Body</th>
-              <th>Response Header</th>
-              <th>Response Body</th>
             </tr>
           </thead>
           <tbody>
-            {results.map((r, idx) => (
-              <tr key={idx}>
-                <td>{r.method}</td>
-                <td>{r.endpoint}</td>
-                <td>{r.statusCode}</td>
-                <td>{r.durationMs}</td>
-                <td>{r.startTime}</td>
-                <td>{r.endTime}</td>
-                <td>{r.isRequestSuccess ? '✅' : '❌'}</td>
-                <td>
-                  <pre>{JSON.stringify(r.requestHeader || r.requestHeaders, null, 2)}</pre>
-                </td>
-                <td>
-                  <pre>{JSON.stringify(r.requestBody, null, 2)}</pre>
-                </td>
-                <td>
-                  <pre>{JSON.stringify(r.responseHeaders, null, 2)}</pre>
-                </td>
-                <td>
-                  <pre>{JSON.stringify(r.responseBody, null, 2)}</pre>
-                </td>
-              </tr>
+            {results.map((item, idx) => (
+              <TableRow key={idx} item={item} />
             ))}
           </tbody>
         </table>
diff --git a/src/pages/dashboard/table-area/TableRow.tsx b/src/pages/dashboard/table-area/TableRow.tsx
new file mode 100644
index 0000000..244902b
--- /dev/null
+++ b/src/pages/dashboard/table-area/TableRow.tsx
@@ -0,0 +1,30 @@
+import React, { useState } from 'react';
+import styles from './TableArea.module.scss';
+import { ResultItem } from '@/types/index.ts';
+import DetailRow from './DetailRow.tsx';
+import LightIndicator from '@/components/LightIndicator.tsx';
+
+interface TableRowProps {
+  item: ResultItem;
+}
+
+const TableRow: React.FC<TableRowProps> = ({ item }) => {
+  const [open, setOpen] = useState(false);
+
+  return (
+    <>
+      <tr onClick={() => setOpen(!open)} className={styles.summaryRow}>
+        <td>
+          <LightIndicator isSuccess={item.isRequestSuccess} />
+        </td>
+        <td>{item.method}</td>
+        <td>{item.endpoint}</td>
+        <td>{item.statusCode}</td>
+        <td>{item.durationMs}</td>
+      </tr>
+      <DetailRow isOpen={open} item={item} />
+    </>
+  );
+};
+
+export default TableRow;


Reviews:
user: login: cod0216
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T14:12:56Z
user: login: haerim-kweon
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-02T01:03:45Z

Review comments:
diff_hunk: @@ -48,3 +60,58 @@
     }
   }
 }
+
+.summaryRow {
+  cursor: pointer;
+  transition: background-color 0.2s;
+
+  &:hover {
+    background-color: $primary-50;
+  }
+}
+
+.detailContentWrapper {
+  max-height: 0;
+  overflow: hidden;
+  transition: max-height 0.8s ease;
+  background-color: $white;
+  text-align: left;
+
+  &.expanded {
+    max-height: 1000px;
+  }
+}
+
+.detailContent {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 2rem;
+  padding: 1rem;
+  border-top: 1px solid $primary-100;
+  background-color: $white;
+
+  @media (max-width: 768px) {
+    grid-template-columns: 1fr;
+  }
+}
+
+.detailColumn {
+  display: flex;
+  flex-direction: column;
+
+  h3 {
+    margin: 0 0 0.5rem;
+  }
+
+  pre {
+    margin: 0;
+    background-color: $gray-50;
+    padding: 0.5rem;
+    border-radius: 4px;
path: src/pages/dashboard/table-area/TableArea.module.scss
user: login: cod0216
body: In the next commit, please use my custom _radius.scss file
created_at: 2025-05-01T14:11:25Z
author_association: MEMBER
diff_hunk: @@ -48,3 +60,58 @@
     }
   }
 }
+
+.summaryRow {
+  cursor: pointer;
+  transition: background-color 0.2s;
+
+  &:hover {
+    background-color: $primary-50;
+  }
+}
+
+.detailContentWrapper {
+  max-height: 0;
+  overflow: hidden;
+  transition: max-height 0.8s ease;
+  background-color: $white;
+  text-align: left;
+
+  &.expanded {
+    max-height: 1000px;
+  }
+}
+
+.detailContent {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 2rem;
+  padding: 1rem;
+  border-top: 1px solid $primary-100;
+  background-color: $white;
+
+  @media (max-width: 768px) {
+    grid-template-columns: 1fr;
+  }
+}
+
+.detailColumn {
+  display: flex;
+  flex-direction: column;
+
+  h3 {
+    margin: 0 0 0.5rem;
+  }
+
+  pre {
+    margin: 0;
+    background-color: $gray-50;
+    padding: 0.5rem;
+    border-radius: 4px;
path: src/pages/dashboard/table-area/TableArea.module.scss
user: login: haerim-kweon
body: I've reviewed it — thank you for defining the shared variable. It's much appreciated!
created_at: 2025-05-02T01:03:45Z
author_association: COLLABORATOR

PR comments:
user: login: cod0216
created_at: 2025-05-01T14:13:47Z
body: Everything else looks great! I’ll ask you any additional questions when we meet. Thanks for all your hard work!
author_association: MEMBER
user: login: haazz
created_at: 2025-05-01T15:11:50Z
body: Thank you for your work!!
I love the design of the UI. It looks fantastic! 🎆🎆
author_association: CONTRIBUTOR
user: login: haerim-kweon
created_at: 2025-05-02T01:05:44Z
body: > Thank you for your work!! I love the design of the UI. It looks fantastic! 🎆🎆

I truly appreciate your comments — your feedback was very helpful!


author_association: COLLABORATOR
user: login: sung-jun98
created_at: 2025-05-02T01:05:56Z
body: Looks great! The design is really well done!
author_association: COLLABORATOR
user: login: haerim-kweon
created_at: 2025-05-02T01:06:30Z
body: > Looks great! The design is really well done!

I really appreciate the compliment — it truly makes me happy!


author_association: COLLABORATOR