This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-7043-084309c355979548ba76b97ba8ef6753bd256ba7
in repository https://gitbox.apache.org/repos/asf/texera.git

commit ac68ea471bc8367d6142ddc9f4ae7df6f1aa3d06
Author: Ranjani Veena Belavadi <[email protected]>
AuthorDate: Thu Jul 30 15:02:13 2026 -0700

    refactor(frontend): extract Data Card and Versions & Files tabs (#7043)
    
    ### What changes were proposed in this PR?
    Extracts the remaining content on the dataset detail page into two new
    tabs: Data Card and Versions & Files. Part of #6394; second of two
    sub-issues (#6395, Settings, already merged).
    
    - **Versions & Files**: pure relocation. The existing file browser,
    version selector, file preview, and upload widget move here unchanged.
    - **Data Card**: new read-only overview, two cards:
    - Description (moved out of the page header, which previously duplicated
    it, the header now shows name/badges/counts only), using the existing
    `texera-markdown-description` component's built-in "View more" collapse
    for long descriptions.
    - Key facts panel: Created date, Last updated (latest version's creation
    date, independent of whatever's selected in Versions & Files), Latest
    version file name, Latest version size, and Versions count.
    
    Before: two tabs- Data Card (holding everything: file browser, version
    history, uploads, plus a duplicated description in the header) and
    Settings.
    
    <img width="1915" height="1018" alt="settings_tab_recording"
    
src="https://github.com/user-attachments/assets/eee5240a-9bc1-493c-8fda-02e2eb2ba759";
    />
    
    After: three tabs- Data Card (read-only overview only), Versions & Files
    (file/version management, split out of the old Data
    Card tab), and Settings (unchanged).
    
    <img width="1918" height="1019" alt="dataset_detail_tab_layout"
    
src="https://github.com/user-attachments/assets/7f4e4f58-336b-4a71-8812-28d79dac2489";
    />
    
    
    ### Any related issues, documentation, discussions?
    Closes #6942
    
    
    ### How was this PR tested?
    Added 7 unit tests to `dataset-detail.component.spec.ts` covering the
    new Data Card logic:
    - "Last updated" derives from the latest version, independent of
    whichever version is selected in Versions & Files.
    - `retrieveLatestVersionFile()` correctly resolves the latest version's
    file name, including nested paths and the no-files case.
    - Selecting a version with an empty file tree no longer throws.
    
    Manually tested on UI:
    - Versions & Files retains all existing functionality (file browsing,
    version switching, file preview, upload) unchanged.
    - Description renders correctly in Data Card, including markdown
    formatting and the "View more" collapse on long descriptions.
    - Settings tab (merged separately in #6395) is unaffected.
    
    
    ### Was this PR authored or co-authored using generative AI tooling?
    Generated-by: Claude Code (Claude Opus 4.8)
    
    ---------
    
    Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
    Co-authored-by: Xuan Gu <[email protected]>
---
 .../dataset-detail.component.html                  | 708 +++++++++++----------
 .../dataset-detail.component.scss                  | 110 ++++
 .../dataset-detail.component.spec.ts               | 198 +++++-
 .../dataset-detail.component.ts                    |  72 ++-
 4 files changed, 745 insertions(+), 343 deletions(-)

diff --git 
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html
 
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html
index f50dcbd0a6..365bdcee51 100644
--- 
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html
+++ 
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html
@@ -82,375 +82,415 @@
         [src]="coverImageUrl"
         alt="Dataset cover" />
     </div>
-
-    <div class="description-section">
-      <texera-markdown-description
-        [description]="datasetDescription"
-        [editable]="false"
-        [enableViewMore]="true">
-      </texera-markdown-description>
-    </div>
   </nz-card>
 </div>
-
 <nz-tabs>
   <nz-tab nzTitle="Data Card">
-    <nz-layout>
-      <nz-content
-        [ngClass]="{'grayed-out': false, 'disabled-click': false}"
-        style="background-color: white">
-        <nz-card>
-          <div style="display: flex; justify-content: space-between; 
align-items: center">
-            <div class="file-info">
-              <h3 class="file-title">
-                <span class="file-title-main">
-                  <b>{{ currentDisplayedFileName }}</b>
-
-                  <button
-                    nz-button
-                    nzType="text"
-                    nzSize="small"
-                    class="copy-path-btn"
-                    nz-tooltip
-                    nzTooltipTitle="Copy file path"
-                    *ngIf="currentDisplayedFileName"
-                    (click)="copyCurrentFilePath()">
-                    <i
-                      nz-icon
-                      nzType="copy"
-                      nzTheme="twotone">
-                    </i>
-                  </button>
-                </span>
+    <div class="data-card-tab-content">
+      <div class="data-card-columns">
+        <nz-card class="data-card data-card-main">
+          <h3 class="data-card-heading">Description</h3>
+          <div class="data-card-description">
+            <texera-markdown-description
+              *ngIf="datasetDescription; else noDescription"
+              [description]="datasetDescription"
+              [editable]="false"
+              [enableViewMore]="true">
+            </texera-markdown-description>
+            <ng-template #noDescription>
+              <span class="empty-description">No description provided</span>
+            </ng-template>
+          </div>
+        </nz-card>
 
-                <span
-                  *ngIf="currentFileSize"
-                  class="file-size">
-                  <i
-                    nz-icon
-                    nzType="file"
-                    nzTheme="outline"
-                    class="icon-file"></i>
-                  {{ formatSize(currentFileSize) }}
-                </span>
-              </h3>
+        <nz-card class="data-card data-card-details">
+          <div class="data-card-stats">
+            <div class="stat-row">
+              <span class="stat-label">Created</span>
+              <span class="stat-value">{{ datasetCreationTime }}</span>
+            </div>
+            <div class="stat-row">
+              <span class="stat-label">Last updated</span>
+              <span class="stat-value">{{ latestVersionCreationTime }}</span>
+            </div>
+            <div class="stat-row">
+              <span class="stat-label">Versions</span>
+              <span class="stat-value">{{ versions.length }}</span>
             </div>
-            <div style="display: flex">
-              <button
-                nz-button
-                *ngIf="selectedVersion"
-                nz-tooltip="Download the file"
-                [disabled]="!isLogin || !isDownloadAllowed()"
-                (click)="onClickDownloadCurrentFile()">
-                <i
-                  nz-icon
-                  nzTheme="outline"
-                  nzType="download">
-                </i>
-              </button>
-              <button
-                nz-button
-                *ngIf="!isMaximized && selectedVersion"
-                nz-tooltip="Maximize View"
-                (click)="onClickScaleTheView()">
-                <i
-                  nz-icon
-                  nzTheme="outline"
-                  nzType="expand">
-                </i>
-              </button>
-              <button
-                nz-button
-                *ngIf="isMaximized && selectedVersion"
-                nz-tooltip="Minimize View"
-                (click)="onClickScaleTheView()">
-                <i
-                  nz-icon
-                  nzTheme="outline"
-                  nzType="compress">
-                </i>
-              </button>
-              <button
-                *ngIf="!isRightBarCollapsed"
-                nz-button
-                nz-tooltip="Hide the right bar"
-                (click)="onClickHideRightBar()">
-                <i
-                  nz-icon
-                  nzTheme="outline"
-                  nzType="right">
-                </i>
-              </button>
-              <button
-                *ngIf="isRightBarCollapsed"
-                nz-button
-                nz-tooltip="Show Tree"
-                (click)="onClickHideRightBar()">
-                <i
-                  nz-icon
-                  nzTheme="outline"
-                  nzType="left">
-                </i>
-              </button>
+            <div class="stat-row">
+              <span class="stat-label">Latest version file</span>
+              <span class="stat-value">{{ latestVersionFileName }}</span>
+            </div>
+            <div class="stat-row">
+              <span class="stat-label">Latest version size</span>
+              <span class="stat-value">{{ formatSize(latestVersionSize) 
}}</span>
             </div>
           </div>
         </nz-card>
-        <nz-empty
-          class="empty-version-indicator"
-          *ngIf="!selectedVersion"
-          nzNotFoundContent="No version is selected"></nz-empty>
+      </div>
+    </div>
+  </nz-tab>
+  <nz-tab nzTitle="Versions & Files">
+    <div class="versions-tab-content">
+      <nz-layout>
+        <nz-content
+          [ngClass]="{'grayed-out': false, 'disabled-click': false}"
+          style="background-color: white">
+          <nz-card>
+            <div style="display: flex; justify-content: space-between; 
align-items: center">
+              <div class="file-info">
+                <h3 class="file-title">
+                  <span class="file-title-main">
+                    <b>{{ currentDisplayedFileName }}</b>
 
-        <texera-user-dataset-file-renderer
-          *ngIf="selectedVersion"
-          [isMaximized]="isMaximized"
-          [did]="did"
-          [dvid]="selectedVersion.dvid"
-          [filePath]="currentDisplayedFileName"
-          [fileSize]="currentFileSize"
-          [isLogin]="isLogin"
-          class="file-renderer">
-        </texera-user-dataset-file-renderer>
-      </nz-content>
-      <nz-sider
-        *ngIf="!isRightBarCollapsed"
-        nzTheme="light"
-        [nzWidth]="siderWidth"
-        nz-resizable
-        [nzMinWidth]="MIN_SIDER_WIDTH"
-        [nzMaxWidth]="MAX_SIDER_WIDTH"
-        (nzResize)="onSideResize($event)">
-        <nz-resize-handle nzDirection="left">
-          <div class="sider-resize-line">
-            <i
-              class="sider-resize-handle"
-              nz-icon
-              nzType="more"
-              nzTheme="outline"></i>
-          </div>
-        </nz-resize-handle>
-        <div class="right-sider">
-          <nz-collapse nzGhost>
-            <nz-collapse-panel
-              nzHeader="Current Versions"
-              nzActive="true">
-              <div class="version-storage">
-                <h6 style="font-weight: lighter; font-size: 0.9em">Choose a 
Version:</h6>
-                <div class="select-and-button-container">
-                  <nz-select
-                    nzShowSearch
-                    nzAllowClear
-                    nzPlaceHolder="Select a version"
-                    (ngModelChange)="onVersionSelected($event)"
-                    [(ngModel)]="selectedVersion">
-                    <nz-option
-                      *ngFor="let version of versions"
-                      [nzValue]="version"
-                      [nzLabel]="version.name"></nz-option>
-                  </nz-select>
-                  <button
-                    nz-button
-                    nz-tooltip="Download Dataset"
-                    (click)="onClickDownloadVersionAsZip()"
-                    *ngIf="selectedVersion"
-                    [disabled]="!isLogin || !isDownloadAllowed()"
-                    class="spaced-button">
-                    <i
-                      nz-icon
-                      nzType="download"
-                      nzTheme="outline"></i>
-                  </button>
-                </div>
-                <ng-container *ngIf="selectedVersion">
-                  <div class="version-size">
-                    <i
-                      nz-icon
-                      nzType="database"
-                      nzTheme="outline"
-                      class="icon-database"></i>
-                    Version Size: {{ formatSize(currentDatasetVersionSize) }}
-                  </div>
-                  <div
-                    *ngIf="selectedVersionCreationTime"
-                    class="version-date">
+                    <button
+                      nz-button
+                      nzType="text"
+                      nzSize="small"
+                      class="copy-path-btn"
+                      nz-tooltip
+                      nzTooltipTitle="Copy file path"
+                      *ngIf="currentDisplayedFileName"
+                      (click)="copyCurrentFilePath()">
+                      <i
+                        nz-icon
+                        nzType="copy"
+                        nzTheme="twotone">
+                      </i>
+                    </button>
+                  </span>
+
+                  <span
+                    *ngIf="currentFileSize"
+                    class="file-size">
                     <i
                       nz-icon
-                      nzType="calendar"
+                      nzType="file"
                       nzTheme="outline"
-                      class="icon-database"></i>
-                    Created at: {{ selectedVersionCreationTime }}
-                  </div>
-                </ng-container>
+                      class="icon-file"></i>
+                    {{ formatSize(currentFileSize) }}
+                  </span>
+                </h3>
               </div>
-              <texera-user-dataset-version-filetree
-                [fileTreeNodes]="fileTreeNodeList"
-                [isTreeNodeDeletable]="true"
-                (selectedTreeNode)="onVersionFileTreeNodeSelected($event)"
-                (deletedTreeNode)="onPreviouslyUploadedFileDeleted($event)"
-                (setCoverImage)="onSetCoverImage($event)">
-              </texera-user-dataset-version-filetree>
-            </nz-collapse-panel>
-          </nz-collapse>
-
-          <nz-divider></nz-divider>
-          <nz-collapse
-            *ngIf="userDatasetAccessLevel === 'WRITE'"
-            nzGhost>
-            <nz-collapse-panel
-              nzActive="true"
-              nzHeader="Create New Version">
-              <texera-user-files-uploader
-                [did]="did"
-                [ownerEmail]="ownerEmail"
-                [datasetName]="datasetName"
-                (uploadedFiles)="onNewUploadFilesChanged($event)">
-              </texera-user-files-uploader>
+              <div style="display: flex">
+                <button
+                  nz-button
+                  *ngIf="selectedVersion"
+                  nz-tooltip="Download the file"
+                  [disabled]="!isLogin || !isDownloadAllowed()"
+                  (click)="onClickDownloadCurrentFile()">
+                  <i
+                    nz-icon
+                    nzTheme="outline"
+                    nzType="download">
+                  </i>
+                </button>
+                <button
+                  nz-button
+                  *ngIf="!isMaximized && selectedVersion"
+                  nz-tooltip="Maximize View"
+                  (click)="onClickScaleTheView()">
+                  <i
+                    nz-icon
+                    nzTheme="outline"
+                    nzType="expand">
+                  </i>
+                </button>
+                <button
+                  nz-button
+                  *ngIf="isMaximized && selectedVersion"
+                  nz-tooltip="Minimize View"
+                  (click)="onClickScaleTheView()">
+                  <i
+                    nz-icon
+                    nzTheme="outline"
+                    nzType="compress">
+                  </i>
+                </button>
+                <button
+                  *ngIf="!isRightBarCollapsed"
+                  nz-button
+                  nz-tooltip="Hide the right bar"
+                  (click)="onClickHideRightBar()">
+                  <i
+                    nz-icon
+                    nzTheme="outline"
+                    nzType="right">
+                  </i>
+                </button>
+                <button
+                  *ngIf="isRightBarCollapsed"
+                  nz-button
+                  nz-tooltip="Show Tree"
+                  (click)="onClickHideRightBar()">
+                  <i
+                    nz-icon
+                    nzTheme="outline"
+                    nzType="left">
+                  </i>
+                </button>
+              </div>
+            </div>
+          </nz-card>
+          <nz-empty
+            class="empty-version-indicator"
+            *ngIf="!selectedVersion"
+            nzNotFoundContent="No version is selected"></nz-empty>
 
-              <nz-collapse
-                nzGhost
-                class="upload-status-panels">
-                <nz-collapse-panel
-                  *ngIf="queuedCount > 0"
-                  [nzHeader]="'Pending: ' + queuedCount + ' file(s)'"
-                  (nzActiveChange)="onPendingPanelActiveChange($event)">
-                  <!-- Virtualized so only visible rows are in the DOM 
(#5586). The viewport
-                   measures height 0 while the panel is collapsed, so it is 
re-measured
-                   on expand. -->
-                  <cdk-virtual-scroll-viewport
-                    class="upload-progress-wrapper-pending"
-                    [itemSize]="PENDING_ROW_HEIGHT_PX"
-                    [minBufferPx]="PENDING_LIST_MAX_HEIGHT_PX"
-                    [maxBufferPx]="2 * PENDING_LIST_MAX_HEIGHT_PX"
-                    [style.height.px]="pendingListHeightPx">
+          <texera-user-dataset-file-renderer
+            *ngIf="selectedVersion"
+            [isMaximized]="isMaximized"
+            [did]="did"
+            [dvid]="selectedVersion.dvid"
+            [filePath]="currentDisplayedFileName"
+            [fileSize]="currentFileSize"
+            [isLogin]="isLogin"
+            class="file-renderer">
+          </texera-user-dataset-file-renderer>
+        </nz-content>
+        <nz-sider
+          *ngIf="!isRightBarCollapsed"
+          nzTheme="light"
+          [nzWidth]="siderWidth"
+          nz-resizable
+          [nzMinWidth]="MIN_SIDER_WIDTH"
+          [nzMaxWidth]="MAX_SIDER_WIDTH"
+          (nzResize)="onSideResize($event)">
+          <nz-resize-handle nzDirection="left">
+            <div class="sider-resize-line">
+              <i
+                class="sider-resize-handle"
+                nz-icon
+                nzType="more"
+                nzTheme="outline"></i>
+            </div>
+          </nz-resize-handle>
+          <div class="right-sider">
+            <nz-collapse nzGhost>
+              <nz-collapse-panel
+                nzHeader="Current Versions"
+                nzActive="true">
+                <div class="version-storage">
+                  <h6 style="font-weight: lighter; font-size: 0.9em">Choose a 
Version:</h6>
+                  <div class="select-and-button-container">
+                    <nz-select
+                      nzShowSearch
+                      nzAllowClear
+                      nzPlaceHolder="Select a version"
+                      (ngModelChange)="onVersionSelected($event)"
+                      [(ngModel)]="selectedVersion">
+                      <nz-option
+                        *ngFor="let version of versions"
+                        [nzValue]="version"
+                        [nzLabel]="version.name"></nz-option>
+                    </nz-select>
+                    <button
+                      nz-button
+                      nz-tooltip="Download Dataset"
+                      (click)="onClickDownloadVersionAsZip()"
+                      *ngIf="selectedVersion"
+                      [disabled]="!isLogin || !isDownloadAllowed()"
+                      class="spaced-button">
+                      <i
+                        nz-icon
+                        nzType="download"
+                        nzTheme="outline"></i>
+                    </button>
+                  </div>
+                  <ng-container *ngIf="selectedVersion">
+                    <div class="version-size">
+                      <i
+                        nz-icon
+                        nzType="database"
+                        nzTheme="outline"
+                        class="icon-database"></i>
+                      Version Size: {{ formatSize(currentDatasetVersionSize) }}
+                    </div>
                     <div
-                      class="pending-file-row"
-                      *cdkVirtualFor="let fileName of queuedFileNames; 
trackBy: trackByPendingFile">
-                      <span
-                        class="pending-file-name"
-                        nz-tooltip
-                        [nzTooltipTitle]="fileName"
-                        >{{ fileName }}</span
-                      >
-                      <button
-                        nz-button
-                        nzType="text"
-                        nzShape="circle"
-                        nz-tooltip
-                        [nzTooltipTitle]="'Remove from queue'"
-                        (click)="cancelExistingUpload(fileName)">
-                        <span
-                          nz-icon
-                          nzType="close"
-                          nzTheme="outline"></span>
-                      </button>
+                      *ngIf="selectedVersionCreationTime"
+                      class="version-date">
+                      <i
+                        nz-icon
+                        nzType="calendar"
+                        nzTheme="outline"
+                        class="icon-database"></i>
+                      Created at: {{ selectedVersionCreationTime }}
                     </div>
-                  </cdk-virtual-scroll-viewport>
-                </nz-collapse-panel>
+                  </ng-container>
+                </div>
+                <texera-user-dataset-version-filetree
+                  [fileTreeNodes]="fileTreeNodeList"
+                  [isTreeNodeDeletable]="true"
+                  (selectedTreeNode)="onVersionFileTreeNodeSelected($event)"
+                  (deletedTreeNode)="onPreviouslyUploadedFileDeleted($event)"
+                  (setCoverImage)="onSetCoverImage($event)">
+                </texera-user-dataset-version-filetree>
+              </nz-collapse-panel>
+            </nz-collapse>
 
-                <nz-divider
-                  class="section-divider"
-                  *ngIf="queuedCount > 0"></nz-divider>
+            <nz-divider></nz-divider>
+            <nz-collapse
+              *ngIf="userDatasetAccessLevel === 'WRITE'"
+              nzGhost>
+              <nz-collapse-panel
+                nzActive="true"
+                nzHeader="Create New Version">
+                <texera-user-files-uploader
+                  [did]="did"
+                  [ownerEmail]="ownerEmail"
+                  [datasetName]="datasetName"
+                  (uploadedFiles)="onNewUploadFilesChanged($event)">
+                </texera-user-files-uploader>
 
-                <nz-collapse-panel
-                  *ngIf="activeCount > 0"
-                  [nzHeader]="'Uploading: ' + activeCount + ' file(s)'">
-                  <div class="upload-progress-wrapper">
-                    <div *ngFor="let task of uploadTasks; trackBy: 
trackByTask">
-                      <div class="progress-header">
-                        <span><b>{{ task.status }}</b>: {{ task.filePath 
}}</span>
+                <nz-collapse
+                  nzGhost
+                  class="upload-status-panels">
+                  <nz-collapse-panel
+                    *ngIf="queuedCount > 0"
+                    [nzHeader]="'Pending: ' + queuedCount + ' file(s)'"
+                    (nzActiveChange)="onPendingPanelActiveChange($event)">
+                    <!-- Virtualized so only visible rows are in the DOM 
(#5586). The viewport
+                   measures height 0 while the panel is collapsed, so it is 
re-measured
+                   on expand. -->
+                    <cdk-virtual-scroll-viewport
+                      class="upload-progress-wrapper-pending"
+                      [itemSize]="PENDING_ROW_HEIGHT_PX"
+                      [minBufferPx]="PENDING_LIST_MAX_HEIGHT_PX"
+                      [maxBufferPx]="2 * PENDING_LIST_MAX_HEIGHT_PX"
+                      [style.height.px]="pendingListHeightPx">
+                      <div
+                        class="pending-file-row"
+                        *cdkVirtualFor="let fileName of queuedFileNames; 
trackBy: trackByPendingFile">
+                        <span
+                          class="pending-file-name"
+                          nz-tooltip
+                          [nzTooltipTitle]="fileName"
+                          >{{ fileName }}</span
+                        >
                         <button
                           nz-button
                           nzType="text"
                           nzShape="circle"
-                          [nz-tooltip]="
-                (task.status === 'aborted' || task.status === 'finished')
-                ? 'Close'
-                : 'Cancel the upload'
-                "
-                          (click)="onClickAbortUploadProgress(task)">
-                          <i
+                          nz-tooltip
+                          [nzTooltipTitle]="'Remove from queue'"
+                          (click)="cancelExistingUpload(fileName)">
+                          <span
                             nz-icon
                             nzType="close"
-                            nzTheme="outline"></i>
+                            nzTheme="outline"></span>
                         </button>
                       </div>
+                    </cdk-virtual-scroll-viewport>
+                  </nz-collapse-panel>
 
-                      <div
-                        class="upload-stats"
-                        *ngIf="task.status !== 'initializing'">
-                        <nz-progress
-                          [nzPercent]="task.percentage"
-                          
[nzStatus]="getUploadStatus(task.status)"></nz-progress>
-                        <nz-tag
-                          *ngIf="task.status === 'uploading'"
-                          [nzColor]="'blue'">
-                          <span class="fixed-width-speed">{{ 
formatSpeed(task.uploadSpeed) }}</span> -
-                          <span class="fixed-width-time">{{ 
formatTime(task.totalTime ?? 0) }}</span> elapsed,
-                          <span class="fixed-width-time">{{ 
formatTime(task.estimatedTimeRemaining ?? 0) }} left</span>
-                        </nz-tag>
+                  <nz-divider
+                    class="section-divider"
+                    *ngIf="queuedCount > 0"></nz-divider>
+
+                  <nz-collapse-panel
+                    *ngIf="activeCount > 0"
+                    [nzHeader]="'Uploading: ' + activeCount + ' file(s)'">
+                    <div class="upload-progress-wrapper">
+                      <div *ngFor="let task of uploadTasks; trackBy: 
trackByTask">
+                        <div class="progress-header">
+                          <span><b>{{ task.status }}</b>: {{ task.filePath 
}}</span>
+                          <button
+                            nz-button
+                            nzType="text"
+                            nzShape="circle"
+                            [nz-tooltip]="
+                (task.status === 'aborted' || task.status === 'finished')
+                ? 'Close'
+                : 'Cancel the upload'
+                "
+                            (click)="onClickAbortUploadProgress(task)">
+                            <i
+                              nz-icon
+                              nzType="close"
+                              nzTheme="outline"></i>
+                          </button>
+                        </div>
+
+                        <div
+                          class="upload-stats"
+                          *ngIf="task.status !== 'initializing'">
+                          <nz-progress
+                            [nzPercent]="task.percentage"
+                            
[nzStatus]="getUploadStatus(task.status)"></nz-progress>
+                          <nz-tag
+                            *ngIf="task.status === 'uploading'"
+                            [nzColor]="'blue'">
+                            <span class="fixed-width-speed">{{ 
formatSpeed(task.uploadSpeed) }}</span> -
+                            <span class="fixed-width-time">{{ 
formatTime(task.totalTime ?? 0) }}</span> elapsed,
+                            <span class="fixed-width-time"
+                              >{{ formatTime(task.estimatedTimeRemaining ?? 0) 
}} left</span
+                            >
+                          </nz-tag>
 
-                        <nz-tag *ngIf="(task.status === 'finished' || 
task.status === 'aborted')">
-                          Upload time: {{ formatTime(task.totalTime ?? 0) }}
-                        </nz-tag>
+                          <nz-tag *ngIf="(task.status === 'finished' || 
task.status === 'aborted')">
+                            Upload time: {{ formatTime(task.totalTime ?? 0) }}
+                          </nz-tag>
+                        </div>
                       </div>
                     </div>
-                  </div>
-                </nz-collapse-panel>
+                  </nz-collapse-panel>
 
-                <nz-divider
-                  class="section-divider"
-                  *ngIf="activeCount > 0"></nz-divider>
+                  <nz-divider
+                    class="section-divider"
+                    *ngIf="activeCount > 0"></nz-divider>
 
-                <nz-collapse-panel
-                  *ngIf="hasAnyActivity"
-                  [nzHeader]="'Finished: ' + pendingChangesCount + ' file(s)'"
-                  (nzActiveChange)="$event && 
stagedObjectsList.remeasureViewport()">
-                  <texera-dataset-staged-objects-list
-                    #stagedObjectsList
-                    [uploadTimeMap]="uploadTimeMap"
-                    [did]="did"
-                    [userMakeChangesEvent]="userMakeChanges"
-                    (stagedObjectsChanged)="onStagedObjectsUpdated($event)">
-                  </texera-dataset-staged-objects-list>
-                </nz-collapse-panel>
-              </nz-collapse>
+                  <nz-collapse-panel
+                    *ngIf="hasAnyActivity"
+                    [nzHeader]="'Finished: ' + pendingChangesCount + ' 
file(s)'"
+                    (nzActiveChange)="$event && 
stagedObjectsList.remeasureViewport()">
+                    <texera-dataset-staged-objects-list
+                      #stagedObjectsList
+                      [uploadTimeMap]="uploadTimeMap"
+                      [did]="did"
+                      [userMakeChangesEvent]="userMakeChanges"
+                      (stagedObjectsChanged)="onStagedObjectsUpdated($event)">
+                    </texera-dataset-staged-objects-list>
+                  </nz-collapse-panel>
+                </nz-collapse>
 
-              <texera-dataset-staged-objects-list
-                *ngIf="!hasAnyActivity"
-                [uploadTimeMap]="uploadTimeMap"
-                [did]="did"
-                [userMakeChangesEvent]="userMakeChanges"
-                (stagedObjectsChanged)="onStagedObjectsUpdated($event)">
-              </texera-dataset-staged-objects-list>
+                <texera-dataset-staged-objects-list
+                  *ngIf="!hasAnyActivity"
+                  [uploadTimeMap]="uploadTimeMap"
+                  [did]="did"
+                  [userMakeChangesEvent]="userMakeChanges"
+                  (stagedObjectsChanged)="onStagedObjectsUpdated($event)">
+                </texera-dataset-staged-objects-list>
 
-              <div
-                *ngIf="userHasWriteAccess() && userHasPendingChanges"
-                class="version-creator">
-                <div class="version-input-container">
-                  <label>Version:</label>
-                  <input
-                    nz-input
-                    [(ngModel)]="versionName"
-                    placeholder="Describe the new version (Optional)"
-                    [disabled]="isCreatingVersion"
-                    (keydown.enter)="onClickOpenVersionCreator()"
-                    class="version-input" />
-                </div>
-                <div>
-                  <button
-                    nz-button
-                    nzType="primary"
-                    [nzLoading]="isCreatingVersion"
-                    (click)="onClickOpenVersionCreator()"
-                    class="create-dataset-version-button">
-                    Submit
-                  </button>
+                <div
+                  *ngIf="userHasWriteAccess() && userHasPendingChanges"
+                  class="version-creator">
+                  <div class="version-input-container">
+                    <label>Version:</label>
+                    <input
+                      nz-input
+                      [(ngModel)]="versionName"
+                      placeholder="Describe the new version (Optional)"
+                      [disabled]="isCreatingVersion"
+                      (keydown.enter)="onClickOpenVersionCreator()"
+                      class="version-input" />
+                  </div>
+                  <div>
+                    <button
+                      nz-button
+                      nzType="primary"
+                      [nzLoading]="isCreatingVersion"
+                      (click)="onClickOpenVersionCreator()"
+                      class="create-dataset-version-button">
+                      Submit
+                    </button>
+                  </div>
                 </div>
-              </div>
-            </nz-collapse-panel>
-          </nz-collapse>
-        </div>
-      </nz-sider>
-    </nz-layout>
+              </nz-collapse-panel>
+            </nz-collapse>
+          </div>
+        </nz-sider>
+      </nz-layout>
+    </div>
   </nz-tab>
   <nz-tab
     nzTitle="Settings"
@@ -481,7 +521,7 @@
 
         <div class="settings-field">
           <label class="settings-field-label">Description</label>
-          <div class="settings-field-hint">Shown on cards and the Metadata 
tab.</div>
+          <div class="settings-field-hint">Shown on cards.</div>
           <texera-markdown-description
             [description]="datasetDescription"
             [editable]="userDatasetAccessLevel === 'WRITE'"
diff --git 
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.scss
 
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.scss
index b5bbf7dfce..c0b17cfee1 100644
--- 
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.scss
+++ 
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.scss
@@ -356,6 +356,18 @@ nz-tabs {
   padding-left: 16px;
 }
 
+.versions-tab-content {
+  height: 100%;
+  overflow-y: auto;
+  padding-bottom: 24px;
+}
+
+.data-card-tab-content {
+  height: 100%;
+  overflow-y: auto;
+  padding-bottom: 24px;
+}
+
 // The Settings tab shares the tab area's bounded height with the Data Card
 // tab. Give its content its own scroll region (mirroring the height:100% +
 // overflow-y:auto pattern used by other dashboard pages) so both cards can
@@ -367,6 +379,104 @@ nz-tabs {
   padding-bottom: 24px;
 }
 
+// Two side-by-side cards filling the full tab width: a wide description card
+// and a narrower stats card. The flex grow ratio is the single source of
+// truth for their proportions (basis 0), so no width/max-width competes.
+.data-card-columns {
+  display: flex;
+  align-items: flex-start;
+  gap: 20px;
+  margin-top: 24px;
+  width: 100%;
+  // Match the tab bar's left inset (.ant-tabs-nav padding-left) so the cards
+  // line up with the tab labels above, not the page's outer edge.
+  padding-left: 16px;
+}
+
+.data-card {
+  border-radius: 8px;
+  border: 1px solid #d9d9d9;
+  // Let each card shrink to its flex share regardless of content width; 
without
+  // this the default min-width:auto keeps them from shrinking, skewing the
+  // ratio and leaving odd empty gaps.
+  min-width: 0;
+}
+
+// Left card takes ~3/4 of the row, stats card ~1/4 — grow ratio with basis 0
+// so the split is exact and content-independent.
+.data-card-main {
+  flex: 3 1 0;
+}
+
+.data-card-details {
+  flex: 1 1 0;
+}
+
+// Stack the two cards full-width on narrow viewports.
+@media (max-width: 768px) {
+  .data-card-columns {
+    flex-direction: column;
+    align-items: stretch;
+  }
+}
+
+// Matches the app's standard card-section heading convention (e.g. the
+// Settings tab's "General" card title): bold, dark, slightly larger than
+// body text, with a clear gap before the content below.
+.data-card-heading {
+  font-size: 18px;
+  font-weight: 600;
+  color: rgba(0, 0, 0, 0.85);
+  margin: 0 0 20px;
+}
+
+// Plain text, no box/border styling — larger size keeps it readable as prose.
+.data-card-description {
+  font-size: 16px;
+  line-height: 1.6;
+  margin-bottom: 32px;
+}
+
+// Empty state: muted grey italic so it reads as "nothing here yet", not 
broken.
+.empty-description {
+  color: rgba(0, 0, 0, 0.4);
+  font-style: italic;
+}
+
+// Stats as a full-width vertical list, one fact per row, avoiding the awkward
+// wrapping of the previous cramped horizontal layout.
+.data-card-stats {
+  display: flex;
+  flex-direction: column;
+}
+
+.stat-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: baseline;
+  gap: 16px;
+  padding: 12px 0;
+  border-bottom: 1px solid #f0f0f0;
+
+  &:last-child {
+    border-bottom: none;
+  }
+}
+
+.stat-label {
+  font-size: 13px;
+  font-weight: 600;
+  color: rgba(0, 0, 0, 0.85);
+  flex-shrink: 0;
+}
+
+.stat-value {
+  font-weight: 400;
+  color: rgba(0, 0, 0, 0.45);
+  text-align: right;
+  word-break: break-word;
+}
+
 .settings-general-card {
   max-width: 700px;
   margin: 24px auto 0;
diff --git 
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts
 
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts
index 3b40c12d09..d7faeaa920 100644
--- 
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts
+++ 
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts
@@ -90,6 +90,18 @@ describe("DatasetDetailComponent upload queue", () => {
               })
             ),
             retrieveDatasetVersionList: vi.fn(() => of([])),
+            retrieveDatasetLatestVersion: vi.fn(() =>
+              of({
+                dvid: 1,
+                did: 1,
+                creatorUid: 1,
+                name: "v1",
+                versionHash: undefined,
+                creationTime: undefined,
+                fileNodes: [],
+              })
+            ),
+            retrieveDatasetVersionFileTree: vi.fn(() => of({ fileNodes: [], 
size: 1024 })),
             getDatasetDiff: vi.fn(() => of([])),
             createDatasetVersion: vi.fn(() => of({})),
             deleteDatasetFile: vi.fn(() => of({})),
@@ -301,7 +313,15 @@ describe("DatasetDetailComponent upload queue", () => {
 
   it("renders the virtualized pending list and re-measures viewports on panel 
expand", async () => {
     dropFiles("f1.txt", "f2.txt", "f3.txt", "f4.txt", "f5.txt");
+
+    // The upload UI lives in the "Versions & Files" tab; nz-tabs does not 
render a
+    // tab's content into the DOM until it has been selected at least once.
+    const tabButtons: NodeListOf<HTMLElement> = 
fixture.nativeElement.querySelectorAll(".ant-tabs-tab-btn");
+    const versionsTab = Array.from(tabButtons).find(tab => 
tab.textContent?.includes("Versions & Files"));
+    expect(versionsTab).toBeTruthy();
+    (versionsTab as HTMLElement).click();
     fixture.detectChanges();
+
     // Flush the viewport's init microtask, then render the rows.
     await Promise.resolve();
     fixture.detectChanges();
@@ -423,6 +443,7 @@ describe("DatasetDetailComponent behavior", () => {
     datasetServiceStub = {
       getDataset: vi.fn(() => of(makeDashboardDataset())),
       retrieveDatasetVersionList: vi.fn(() => of([])),
+      retrieveDatasetLatestVersion: vi.fn(() => of(makeVersion())),
       getDatasetCoverUrl: vi.fn(() => of({ url: "http://cover"; })),
       retrieveDatasetVersionFileTree: vi.fn(() => of({ fileNodes: 
[fileLeaf("a.txt", "/root", 1)], size: 1 })),
       createDatasetVersion: vi.fn(() => of(makeVersion())),
@@ -465,6 +486,7 @@ describe("DatasetDetailComponent behavior", () => {
 
       expect(datasetServiceStub.getDataset).toHaveBeenCalled();
       expect(datasetServiceStub.retrieveDatasetVersionList).toHaveBeenCalled();
+      
expect(datasetServiceStub.retrieveDatasetLatestVersion).toHaveBeenCalled();
       expect(component.likeCount).toBe(7);
       expect(component.viewCount).toBe(42);
       expect(hubServiceStub.isLiked).not.toHaveBeenCalled();
@@ -623,6 +645,178 @@ describe("DatasetDetailComponent behavior", () => {
 
       
expect(datasetServiceStub.retrieveDatasetVersionFileTree).not.toHaveBeenCalled();
     });
+
+    it("does not throw and leaves the displayed file untouched when the 
version has no files", () => {
+      datasetServiceStub.retrieveDatasetVersionFileTree.mockReturnValue(of({ 
fileNodes: [], size: 0 }));
+
+      createComponent();
+      component.did = 5;
+      component.currentDisplayedFileName = "stale.txt";
+      component.currentFileSize = 99;
+
+      expect(() => component.onVersionSelected(makeVersion({ dvid: 2 
}))).not.toThrow();
+
+      expect(component.fileTreeNodeList).toEqual([]);
+      expect(component.currentDatasetVersionSize).toBe(0);
+      expect(component.currentDisplayedFileName).toBe("stale.txt");
+      expect(component.currentFileSize).toBe(99);
+    });
+  });
+
+  describe("retrieveLatestVersionFile", () => {
+    it("fetches the latest version independently and sets 
latestVersionFileName to the first leaf file", () => {
+      const leaf = fileLeaf("b.txt", "/root", 7);
+      
datasetServiceStub.retrieveDatasetLatestVersion.mockReturnValue(of(makeVersion({
 fileNodes: [leaf] })));
+
+      createComponent();
+      component.did = 5;
+      component.retrieveLatestVersionFile();
+
+      
expect(datasetServiceStub.retrieveDatasetLatestVersion).toHaveBeenCalledWith(5);
+      
expect(component.latestVersionFileName).toBe(getFullPathFromDatasetFileNode(leaf));
+    });
+
+    it("walks nested directories to find the first leaf file", () => {
+      const leaf = fileLeaf("c.txt", "/root/a", 3);
+      const tree: DatasetFileNode[] = [{ name: "a", type: "directory", 
parentDir: "/root", children: [leaf] }];
+      
datasetServiceStub.retrieveDatasetLatestVersion.mockReturnValue(of(makeVersion({
 fileNodes: tree })));
+
+      createComponent();
+      component.did = 5;
+      component.retrieveLatestVersionFile();
+
+      
expect(component.latestVersionFileName).toBe(getFullPathFromDatasetFileNode(leaf));
+    });
+
+    it("sets latestVersionFileName to an empty string when the latest version 
has no files", () => {
+      
datasetServiceStub.retrieveDatasetLatestVersion.mockReturnValue(of(makeVersion()));
+
+      createComponent();
+      component.did = 5;
+      component.retrieveLatestVersionFile();
+
+      expect(component.latestVersionFileName).toBe("");
+    });
+
+    it("derives latestVersionCreationTime from the latest version's 
creationTime", () => {
+      datasetServiceStub.retrieveDatasetLatestVersion.mockReturnValue(
+        of(makeVersion({ dvid: 3, creationTime: CREATION_TS }))
+      );
+
+      createComponent();
+      component.did = 5;
+      component.retrieveLatestVersionFile();
+
+      expect(component.latestVersionCreationTime).toEqual(format(new 
Date(CREATION_TS), "MM/dd/yyyy HH:mm:ss"));
+    });
+
+    it("leaves latestVersionCreationTime empty when the latest version has no 
creation time", () => {
+      
datasetServiceStub.retrieveDatasetLatestVersion.mockReturnValue(of(makeVersion({
 creationTime: undefined })));
+
+      createComponent();
+      component.did = 5;
+      component.retrieveLatestVersionFile();
+
+      expect(component.latestVersionCreationTime).toBe("");
+    });
+
+    it("sets latestVersionSize from a file-tree fetch for the latest version's 
dvid", () => {
+      
datasetServiceStub.retrieveDatasetLatestVersion.mockReturnValue(of(makeVersion({
 dvid: 7 })));
+      datasetServiceStub.retrieveDatasetVersionFileTree.mockReturnValue(of({ 
fileNodes: [], size: 4096 }));
+
+      createComponent();
+      component.did = 5;
+      component.retrieveLatestVersionFile();
+
+      
expect(datasetServiceStub.retrieveDatasetVersionFileTree).toHaveBeenCalledWith(5,
 7, expect.anything());
+      expect(component.latestVersionSize).toBe(4096);
+    });
+
+    it("does not fetch a size when the latest version has no dvid", () => {
+      
datasetServiceStub.retrieveDatasetLatestVersion.mockReturnValue(of(makeVersion({
 dvid: undefined })));
+
+      createComponent();
+      component.did = 5;
+      component.retrieveLatestVersionFile();
+
+      
expect(datasetServiceStub.retrieveDatasetVersionFileTree).not.toHaveBeenCalled();
+      expect(component.latestVersionSize).toBeUndefined();
+    });
+
+    it("clears a previously fetched latestVersionSize when the latest version 
has no dvid", () => {
+      
datasetServiceStub.retrieveDatasetLatestVersion.mockReturnValue(of(makeVersion({
 dvid: 7 })));
+      datasetServiceStub.retrieveDatasetVersionFileTree.mockReturnValue(of({ 
fileNodes: [], size: 4096 }));
+
+      createComponent();
+      component.did = 5;
+      component.retrieveLatestVersionFile();
+
+      expect(component.latestVersionSize).toBe(4096);
+
+      // Without a dvid there is no size to show, so the stale one must not 
linger.
+      
datasetServiceStub.retrieveDatasetLatestVersion.mockReturnValue(of(makeVersion({
 dvid: undefined })));
+      component.retrieveLatestVersionFile();
+
+      expect(component.latestVersionSize).toBeUndefined();
+    });
+
+    it("ignores a superseded call's size response that resolves after a newer 
one", () => {
+      // The first call's file-tree request never completes before the second 
starts.
+      const pendingTree = new Subject<{ fileNodes: DatasetFileNode[]; size: 
number }>();
+      
datasetServiceStub.retrieveDatasetLatestVersion.mockReturnValue(of(makeVersion({
 dvid: 7 })));
+      
datasetServiceStub.retrieveDatasetVersionFileTree.mockReturnValue(pendingTree);
+
+      createComponent();
+      component.did = 5;
+      component.retrieveLatestVersionFile();
+
+      expect(component.latestVersionSize).toBeUndefined();
+
+      // A second call supersedes the first and resolves immediately.
+      datasetServiceStub.retrieveDatasetVersionFileTree.mockReturnValue(of({ 
fileNodes: [], size: 200 }));
+      component.retrieveLatestVersionFile();
+
+      expect(component.latestVersionSize).toBe(200);
+
+      // The superseded response arriving late must not overwrite the fresher 
size.
+      pendingTree.next({ fileNodes: [], size: 999 });
+
+      expect(component.latestVersionSize).toBe(200);
+    });
+
+    it("keeps the latest-version facts fixed when a different version is later 
selected", () => {
+      datasetServiceStub.retrieveDatasetLatestVersion.mockReturnValue(
+        of(makeVersion({ dvid: 10, creationTime: CREATION_TS }))
+      );
+      datasetServiceStub.retrieveDatasetVersionFileTree.mockReturnValue(of({ 
fileNodes: [], size: 500 }));
+
+      createComponent();
+      component.did = 5;
+      component.retrieveLatestVersionFile();
+
+      expect(component.latestVersionSize).toBe(500);
+      expect(component.latestVersionCreationTime).toEqual(format(new 
Date(CREATION_TS), "MM/dd/yyyy HH:mm:ss"));
+
+      // Selecting an older version updates only the selection-scoped values; 
the
+      // Data Card's latest-version facts stay pinned to the latest version.
+      datasetServiceStub.retrieveDatasetVersionFileTree.mockReturnValue(of({ 
fileNodes: [], size: 99 }));
+      component.onVersionSelected(makeVersion({ dvid: 9, creationTime: 
CREATION_TS - 1000 }));
+
+      expect(component.currentDatasetVersionSize).toBe(99);
+      expect(component.selectedVersionCreationTime).toEqual(
+        format(new Date(CREATION_TS - 1000), "MM/dd/yyyy HH:mm:ss")
+      );
+      expect(component.latestVersionSize).toBe(500);
+      expect(component.latestVersionCreationTime).toEqual(format(new 
Date(CREATION_TS), "MM/dd/yyyy HH:mm:ss"));
+    });
+
+    it("does nothing when there is no did", () => {
+      createComponent();
+      component.did = undefined;
+      component.retrieveLatestVersionFile();
+
+      
expect(datasetServiceStub.retrieveDatasetLatestVersion).not.toHaveBeenCalled();
+    });
   });
 
   describe("isDownloadAllowed and userHasWriteAccess", () => {
@@ -727,6 +921,7 @@ describe("DatasetDetailComponent behavior", () => {
       expect(component.versionName).toBe("");
       expect(component.isCreatingVersion).toBe(false);
       expect(datasetServiceStub.retrieveDatasetVersionList).toHaveBeenCalled();
+      
expect(datasetServiceStub.retrieveDatasetLatestVersion).toHaveBeenCalled();
       expect(emit).toHaveBeenCalled();
     });
 
@@ -1159,7 +1354,8 @@ describe("DatasetDetailComponent behavior", () => {
 
       const tabButtons: NodeListOf<HTMLElement> = 
fixture.nativeElement.querySelectorAll(".ant-tabs-tab-btn");
       const settingsTab = Array.from(tabButtons).find(tab => 
tab.textContent?.includes("Settings"));
-      settingsTab?.click();
+      expect(settingsTab).toBeTruthy();
+      (settingsTab as HTMLElement).click();
       fixture.detectChanges();
 
       return fixture.nativeElement.querySelector('button[title="Delete"]') as 
HTMLButtonElement;
diff --git 
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts
 
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts
index 3581459670..142d6a5e63 100644
--- 
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts
+++ 
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts
@@ -46,7 +46,7 @@ import { DatasetStagedObject } from 
"../../../../../common/type/dataset-staged-o
 import { NzModalService } from "ng-zorro-antd/modal";
 import { AdminSettingsService } from 
"../../../../service/admin/settings/admin-settings.service";
 import { HttpErrorResponse, HttpStatusCode } from "@angular/common/http";
-import { Subscription } from "rxjs";
+import { EMPTY, Subscription } from "rxjs";
 import { formatCount, formatSpeed, formatTime, parseIntOrDefault } from 
"src/app/common/util/format.util";
 import { format } from "date-fns";
 import { NgIf, NgClass, NgFor } from "@angular/common";
@@ -64,6 +64,7 @@ import { MarkdownDescriptionComponent } from 
"../../markdown-description/markdow
 import { NzLayoutComponent, NzContentComponent, NzSiderComponent } from 
"ng-zorro-antd/layout";
 import { NzWaveDirective } from "ng-zorro-antd/core/wave";
 import { NzEmptyComponent } from "ng-zorro-antd/empty";
+import { NzTabsComponent, NzTabComponent } from "ng-zorro-antd/tabs";
 import { UserDatasetFileRendererComponent } from 
"./user-dataset-file-renderer/user-dataset-file-renderer.component";
 import { NzCollapseComponent, NzCollapsePanelComponent } from 
"ng-zorro-antd/collapse";
 import { NzSelectComponent, NzOptionComponent } from "ng-zorro-antd/select";
@@ -74,7 +75,6 @@ import { NzProgressComponent } from "ng-zorro-antd/progress";
 import { UserDatasetStagedObjectsListComponent } from 
"./user-dataset-staged-objects-list/user-dataset-staged-objects-list.component";
 import { NzInputDirective } from "ng-zorro-antd/input";
 import { CdkFixedSizeVirtualScroll, CdkVirtualForOf, CdkVirtualScrollViewport 
} from "@angular/cdk/scrolling";
-import { NzTabsComponent, NzTabComponent } from "ng-zorro-antd/tabs";
 
 export const THROTTLE_TIME_MS = 1000;
 export const ABORT_RETRY_MAX_ATTEMPTS = 10;
@@ -103,6 +103,8 @@ export const ABORT_RETRY_BACKOFF_BASE_MS = 100;
     NzContentComponent,
     NzWaveDirective,
     NzEmptyComponent,
+    NzTabsComponent,
+    NzTabComponent,
     UserDatasetFileRendererComponent,
     NzSiderComponent,
     NzResizableDirective,
@@ -121,8 +123,6 @@ export const ABORT_RETRY_BACKOFF_BASE_MS = 100;
     CdkVirtualScrollViewport,
     CdkFixedSizeVirtualScroll,
     CdkVirtualForOf,
-    NzTabsComponent,
-    NzTabComponent,
   ],
 })
 export class DatasetDetailComponent implements OnInit {
@@ -150,6 +150,14 @@ export class DatasetDetailComponent implements OnInit {
   public selectedVersion: DatasetVersion | undefined;
   public fileTreeNodeList: DatasetFileNode[] = [];
   public selectedVersionCreationTime: string = "";
+  // The following three fields describe the latest version for the Data Card, 
all
+  // sourced from the single retrieveDatasetLatestVersion response so they stay
+  // mutually consistent and independent of the version selected in Versions & 
Files.
+  public latestVersionCreationTime: string = "";
+  public latestVersionFileName: string = "";
+  public latestVersionSize: number | undefined;
+  // Holds the in-flight latest-version fetch so a later call can supersede it.
+  private latestVersionFileSubscription: Subscription | undefined;
 
   public versionCreatorBaseVersion: DatasetVersion | undefined;
   public isLogin: boolean = this.userService.isLogin();
@@ -241,6 +249,7 @@ export class DatasetDetailComponent implements OnInit {
           this.did = params["did"];
           this.retrieveDatasetInfo();
           this.retrieveDatasetVersionList();
+          this.retrieveLatestVersionFile();
           return this.route.data; // or some other observable
         }),
         untilDestroyed(this)
@@ -297,6 +306,7 @@ export class DatasetDetailComponent implements OnInit {
             this.unconfirmedStagedPaths.clear();
             this.refreshPendingChanges();
             this.retrieveDatasetVersionList();
+            this.retrieveLatestVersionFile();
             this.userMakeChanges.emit();
           },
           error: (res: unknown) => {
@@ -421,6 +431,43 @@ export class DatasetDetailComponent implements OnInit {
     }
   }
 
+  // Fetches the latest version independently of the current selection and 
derives
+  // the Data Card's latest-version facts from that single response: the file 
name
+  // and created date directly, and the total size via a follow-up file-tree 
fetch
+  // for the latest version's dvid (mirroring onVersionSelected's size lookup).
+  retrieveLatestVersionFile() {
+    if (this.did) {
+      const did = this.did;
+      // Both fetches live in one subscription (chained with switchMap rather 
than
+      // nested subscribes) so dropping it cancels whichever is still in 
flight:
+      // a call started here supersedes any earlier one, and a slow response 
from
+      // the superseded call can no longer overwrite fresher facts out of 
order.
+      this.latestVersionFileSubscription?.unsubscribe();
+      this.latestVersionFileSubscription = this.datasetService
+        .retrieveDatasetLatestVersion(did)
+        .pipe(
+          switchMap(version => {
+            const firstFile = this.getFirstFileNode(version.fileNodes ?? []);
+            this.latestVersionFileName = firstFile ? 
getFullPathFromDatasetFileNode(firstFile) : "";
+            this.latestVersionCreationTime =
+              typeof version.creationTime === "number"
+                ? format(new Date(version.creationTime), "MM/dd/yyyy HH:mm:ss")
+                : "";
+            if (!version.dvid) {
+              // Nothing to size: clear rather than keep a previous call's 
size.
+              this.latestVersionSize = undefined;
+              return EMPTY;
+            }
+            return this.datasetService.retrieveDatasetVersionFileTree(did, 
version.dvid, this.isLogin);
+          }),
+          untilDestroyed(this)
+        )
+        .subscribe(data => {
+          this.latestVersionSize = data.size;
+        });
+    }
+  }
+
   loadFileContent(node: DatasetFileNode) {
     this.currentDisplayedFileName = getFullPathFromDatasetFileNode(node);
     this.currentFileSize = node.size;
@@ -479,14 +526,23 @@ export class DatasetDetailComponent implements OnInit {
             const date = new Date(version.creationTime);
             this.selectedVersionCreationTime = format(date, "MM/dd/yyyy 
HH:mm:ss");
           }
-          let currentNode = this.fileTreeNodeList[0];
-          while (currentNode.type === "directory" && currentNode.children) {
-            currentNode = currentNode.children[0];
+          const currentNode = this.getFirstFileNode(this.fileTreeNodeList);
+          if (currentNode) {
+            this.loadFileContent(currentNode);
           }
-          this.loadFileContent(currentNode);
         });
   }
 
+  // Walk from the first node into directories until reaching a file, 
returning a
+  // representative leaf file node (or undefined if the tree has no files).
+  private getFirstFileNode(nodes: DatasetFileNode[]): DatasetFileNode | 
undefined {
+    let currentNode: DatasetFileNode | undefined = nodes[0];
+    while (currentNode && currentNode.type === "directory" && 
currentNode.children) {
+      currentNode = currentNode.children[0];
+    }
+    return currentNode;
+  }
+
   onVersionFileTreeNodeSelected(node: DatasetFileNode) {
     this.loadFileContent(node);
   }

Reply via email to