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-6493-e08d3e6b423b960aa95a41d97b6275b7818b9476
in repository https://gitbox.apache.org/repos/asf/texera.git

commit c2d4a262a2ded9ca099c3d2fa81e01b2de16757c
Author: Ranjani Veena Belavadi <[email protected]>
AuthorDate: Mon Jul 27 14:59:03 2026 -0700

    refactor(frontend): extract dataset settings into Settings tab (#6493)
    
    ### What changes were proposed in this PR?
    Extracted the dataset settings/admin functionality on the dataset detail
    page into a dedicated Settings tab, alongside existing content (now
    under a Data Card tab). Part of the parent tab-based layout refactor
    (#6394).
    
    Visibility toggle, downloadable toggle, and description editor moved
    from the header's gear-icon popover into the new Settings tab. The gear
    icon and popover are removed.
    Added dataset rename (input + Save), calling the existing
    `DatasetService.updateDatasetName()` and reusing the name-sanitization
    logic already used in the dataset version creator.
    Added dataset delete, calling the existing
    `DatasetService.deleteDatasets()` with the same `nz-popconfirm`
    confirmation pattern used on the Datasets list page; redirects to the
    list on success.
    
    Before: settings were scattered, visibility/downloadable behind a gear
    icon, description editable only in the header, rename/delete only
    reachable from the Datasets list page.
    <img width="1915" height="1015" alt="settings_tab_recording_old"
    
src="https://github.com/user-attachments/assets/3d29d0f3-0fbb-4caa-8c8f-ee0348df0ab1";
    />
    
    
    After: all dataset settings are consolidated in one Settings tab on the
    detail page itself.
    <img width="1915" height="1018" alt="settings_tab_recording"
    
src="https://github.com/user-attachments/assets/348effd0-e8fd-4583-baff-118af9c83fee";
    />
    
    
    
    ### Any related issues, documentation, discussions?
    Closes #6395
    
    
    ### How was this PR tested?
    
    Added 7 unit tests to `dataset-detail.component.spec.ts` covering the
    two new methods (`onSaveDatasetName`, `onDeleteDataset`): name
    sanitization, success/error paths, and navigation on delete. The
    relocated toggles/description editor already had existing test coverage
    prior to this PR, so no new tests were needed there. 66/66 tests passing
    (59 pre-existing, 7 new).
    
    Also manually tested on UI, since the relocated functionality itself is
    unchanged behavior:
    - Renamed a dataset; confirmed persistence and correct sanitization of
    invalid characters.
    - Edited and saved the description; confirmed it reflects on the
    Datasets list page.
    - Toggled visibility and downloadable; confirmed state persists and
    header badges update accordingly.
    - Deleted a test dataset; confirmed confirmation prompt, successful
    deletion, and redirect to the Datasets list with the dataset removed.
    - Confirmed the Data Card tab (file browser, version history, uploads)
    is unaffected.
    
    
    ### Was this PR authored or co-authored using generative AI tooling?
    Generated-by: Claude Code (Claude Opus 4.8)
    
    ---------
    
    Signed-off-by: Ranjani Veena Belavadi 
<[email protected]>
    Co-authored-by: Xuan Gu <[email protected]>
---
 .../dataset-detail.component.html                  | 770 +++++++++++----------
 .../dataset-detail.component.scss                  |  77 +++
 .../dataset-detail.component.spec.ts               | 145 +++-
 .../dataset-detail.component.ts                    |  66 +-
 4 files changed, 703 insertions(+), 355 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 3c2a031617..f50dcbd0a6 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
@@ -74,47 +74,6 @@
               nzTheme="outline"></i>
             {{ formatCount(likeCount) }}
           </nz-tag>
-
-          <button
-            *ngIf="userHasWriteAccess()"
-            nz-button
-            nzType="text"
-            class="tag-settings-btn"
-            nz-popover
-            nzPopoverTrigger="click"
-            nzPopoverPlacement="bottomRight"
-            [nzPopoverContent]="tagSettingsPopover"
-            nz-tooltip
-            nzTooltipTitle="Settings">
-            <i
-              nz-icon
-              nzType="setting"
-              nzTheme="outline"></i>
-          </button>
-
-          <ng-template #tagSettingsPopover>
-            <div>
-              <div class="tag-settings-title">Quick Settings</div>
-              <div class="tag-settings-row">
-                <span>Visibility:</span>
-                <nz-switch
-                  [ngModel]="datasetIsPublic"
-                  (ngModelChange)="onPublicStatusChange($event)"
-                  [nzDisabled]="userDatasetAccessLevel !== 'WRITE'"
-                  nzCheckedChildren="public"
-                  nzUnCheckedChildren="private"></nz-switch>
-              </div>
-              <div class="tag-settings-row">
-                <span>Downloadable:</span>
-                <nz-switch
-                  [ngModel]="datasetIsDownloadable"
-                  (ngModelChange)="onDownloadableStatusChange($event)"
-                  [nzDisabled]="!isOwner"
-                  nzCheckedChildren="allowed"
-                  nzUnCheckedChildren="blocked"></nz-switch>
-              </div>
-            </div>
-          </ng-template>
         </div>
       </div>
       <img
@@ -127,366 +86,477 @@
     <div class="description-section">
       <texera-markdown-description
         [description]="datasetDescription"
-        [editable]="userDatasetAccessLevel === 'WRITE'"
-        [enableViewMore]="true"
-        (descriptionChange)="onDatasetDescriptionChange($event)">
+        [editable]="false"
+        [enableViewMore]="true">
       </texera-markdown-description>
     </div>
   </nz-card>
 </div>
-<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>
 
+<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>
+
+                <span
+                  *ngIf="currentFileSize"
+                  class="file-size">
+                  <i
+                    nz-icon
+                    nzType="file"
+                    nzTheme="outline"
+                    class="icon-file"></i>
+                  {{ formatSize(currentFileSize) }}
+                </span>
+              </h3>
+            </div>
+            <div style="display: flex">
               <button
                 nz-button
-                nzType="text"
-                nzSize="small"
-                class="copy-path-btn"
-                nz-tooltip
-                nzTooltipTitle="Copy file path"
-                *ngIf="currentDisplayedFileName"
-                (click)="copyCurrentFilePath()">
+                *ngIf="selectedVersion"
+                nz-tooltip="Download the file"
+                [disabled]="!isLogin || !isDownloadAllowed()"
+                (click)="onClickDownloadCurrentFile()">
                 <i
                   nz-icon
-                  nzType="copy"
-                  nzTheme="twotone">
+                  nzTheme="outline"
+                  nzType="download">
                 </i>
               </button>
-            </span>
-
-            <span
-              *ngIf="currentFileSize"
-              class="file-size">
-              <i
-                nz-icon
-                nzType="file"
-                nzTheme="outline"
-                class="icon-file"></i>
-              {{ formatSize(currentFileSize) }}
-            </span>
-          </h3>
-        </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>
-      </div>
-    </nz-card>
-    <nz-empty
-      class="empty-version-indicator"
-      *ngIf="!selectedVersion"
-      nzNotFoundContent="No version is selected"></nz-empty>
-
-    <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">
+                *ngIf="!isMaximized && selectedVersion"
+                nz-tooltip="Maximize View"
+                (click)="onClickScaleTheView()">
                 <i
                   nz-icon
-                  nzType="download"
-                  nzTheme="outline"></i>
+                  nzTheme="outline"
+                  nzType="expand">
+                </i>
               </button>
-            </div>
-            <ng-container *ngIf="selectedVersion">
-              <div class="version-size">
+              <button
+                nz-button
+                *ngIf="isMaximized && selectedVersion"
+                nz-tooltip="Minimize View"
+                (click)="onClickScaleTheView()">
                 <i
                   nz-icon
-                  nzType="database"
                   nzTheme="outline"
-                  class="icon-database"></i>
-                Version Size: {{ formatSize(currentDatasetVersionSize) }}
-              </div>
-              <div
-                *ngIf="selectedVersionCreationTime"
-                class="version-date">
+                  nzType="compress">
+                </i>
+              </button>
+              <button
+                *ngIf="!isRightBarCollapsed"
+                nz-button
+                nz-tooltip="Hide the right bar"
+                (click)="onClickHideRightBar()">
                 <i
                   nz-icon
-                  nzType="calendar"
                   nzTheme="outline"
-                  class="icon-database"></i>
-                Created at: {{ selectedVersionCreationTime }}
-              </div>
-            </ng-container>
+                  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>
-          <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>
+        </nz-card>
+        <nz-empty
+          class="empty-version-indicator"
+          *ngIf="!selectedVersion"
+          nzNotFoundContent="No version is selected"></nz-empty>
 
-          <nz-collapse
-            nzGhost
-            class="upload-status-panels">
+        <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
-              *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
-                  >
+              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
-                    nzType="text"
-                    nzShape="circle"
-                    nz-tooltip
-                    [nzTooltipTitle]="'Remove from queue'"
-                    (click)="cancelExistingUpload(fileName)">
-                    <span
+                    nz-tooltip="Download Dataset"
+                    (click)="onClickDownloadVersionAsZip()"
+                    *ngIf="selectedVersion"
+                    [disabled]="!isLogin || !isDownloadAllowed()"
+                    class="spaced-button">
+                    <i
                       nz-icon
-                      nzType="close"
-                      nzTheme="outline"></span>
+                      nzType="download"
+                      nzTheme="outline"></i>
                   </button>
                 </div>
-              </cdk-virtual-scroll-viewport>
+                <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">
+                    <i
+                      nz-icon
+                      nzType="calendar"
+                      nzTheme="outline"
+                      class="icon-database"></i>
+                    Created at: {{ selectedVersionCreationTime }}
+                  </div>
+                </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
-              *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]="
+              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
+                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
+                        [nzTooltipTitle]="'Remove from queue'"
+                        (click)="cancelExistingUpload(fileName)">
+                        <span
+                          nz-icon
+                          nzType="close"
+                          nzTheme="outline"></span>
+                      </button>
+                    </div>
+                  </cdk-virtual-scroll-viewport>
+                </nz-collapse-panel>
+
+                <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>
+                          (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>
+                      <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>
-              </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
+                *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>
+              </div>
             </nz-collapse-panel>
           </nz-collapse>
+        </div>
+      </nz-sider>
+    </nz-layout>
+  </nz-tab>
+  <nz-tab
+    nzTitle="Settings"
+    *ngIf="userHasWriteAccess()">
+    <div class="settings-tab-content">
+      <nz-card class="settings-general-card">
+        <h3 class="settings-card-title">General</h3>
 
-          <texera-dataset-staged-objects-list
-            *ngIf="!hasAnyActivity"
-            [uploadTimeMap]="uploadTimeMap"
-            [did]="did"
-            [userMakeChangesEvent]="userMakeChanges"
-            (stagedObjectsChanged)="onStagedObjectsUpdated($event)">
-          </texera-dataset-staged-objects-list>
+        <div class="settings-name-row">
+          <div class="settings-name-label">
+            <label>Dataset name</label>
+            <p class="settings-hint">Lowercase letters, numbers and 
hyphens.</p>
+          </div>
+          <div class="settings-name-controls">
+            <input
+              nz-input
+              [(ngModel)]="editedDatasetName" />
+            <button
+              nz-button
+              nzType="primary"
+              (click)="onSaveDatasetName()">
+              Save
+            </button>
+          </div>
+        </div>
 
-          <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>
+        <nz-divider></nz-divider>
+
+        <div class="settings-field">
+          <label class="settings-field-label">Description</label>
+          <div class="settings-field-hint">Shown on cards and the Metadata 
tab.</div>
+          <texera-markdown-description
+            [description]="datasetDescription"
+            [editable]="userDatasetAccessLevel === 'WRITE'"
+            [enableViewMore]="true"
+            (descriptionChange)="onDatasetDescriptionChange($event)">
+          </texera-markdown-description>
+        </div>
+      </nz-card>
+
+      <nz-card class="settings-general-card">
+        <h3 class="settings-card-title">Access &amp; visibility</h3>
+
+        <div class="settings-name-row">
+          <div class="settings-name-label">
+            <label>Visibility</label>
+            <p class="settings-hint">
+              {{ datasetIsPublic ? "Public — anyone can view this dataset." : 
"Private — only you and invited
+              collaborators can see this dataset." }}
+            </p>
+          </div>
+          <div class="settings-name-controls">
+            <nz-switch
+              [ngModel]="datasetIsPublic"
+              (ngModelChange)="onPublicStatusChange($event)"
+              [nzDisabled]="userDatasetAccessLevel !== 'WRITE'"
+              nzCheckedChildren="public"
+              nzUnCheckedChildren="private"></nz-switch>
+          </div>
+        </div>
+
+        <nz-divider></nz-divider>
+
+        <div class="settings-name-row">
+          <div class="settings-name-label">
+            <label>Downloadable</label>
+            <p class="settings-hint">
+              {{ datasetIsDownloadable ? "Viewers can download this dataset." 
: "Viewers can browse files but cannot
+              download them." }}
+            </p>
+          </div>
+          <div class="settings-name-controls">
+            <nz-switch
+              [ngModel]="datasetIsDownloadable"
+              (ngModelChange)="onDownloadableStatusChange($event)"
+              [nzDisabled]="!isOwner"
+              nzCheckedChildren="allowed"
+              nzUnCheckedChildren="blocked"></nz-switch>
           </div>
-        </nz-collapse-panel>
-      </nz-collapse>
+        </div>
+      </nz-card>
+
+      <nz-card class="settings-general-card">
+        <h3 class="settings-card-title">Delete</h3>
+
+        <div class="settings-name-row">
+          <div class="settings-name-label">
+            <label>Delete dataset</label>
+            <p class="settings-hint">This permanently deletes the dataset and 
all of its versions.</p>
+          </div>
+          <div class="settings-name-controls">
+            <button
+              nz-button
+              nzType="text"
+              title="Delete"
+              [disabled]="!isOwner"
+              (nzOnConfirm)="onDeleteDataset()"
+              nz-popconfirm
+              nzPopconfirmTitle="Confirm to delete this dataset.">
+              <i
+                nz-icon
+                nzType="delete"></i>
+            </button>
+          </div>
+        </div>
+      </nz-card>
     </div>
-  </nz-sider>
-</nz-layout>
+  </nz-tab>
+</nz-tabs>
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 d5ef708f88..b5bbf7dfce 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
@@ -347,3 +347,80 @@ nz-select {
   border-radius: 8px;
   margin-right: 80px;
 }
+
+nz-tabs {
+  margin-top: 6px;
+}
+
+::ng-deep .ant-tabs-nav {
+  padding-left: 16px;
+}
+
+// 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
+// be scrolled fully into view, with bottom padding for breathing room below
+// the last card.
+.settings-tab-content {
+  height: 100%;
+  overflow-y: auto;
+  padding-bottom: 24px;
+}
+
+.settings-general-card {
+  max-width: 700px;
+  margin: 24px auto 0;
+  border-radius: 16px;
+  border: 1px solid #d9d9d9;
+
+  .settings-card-title {
+    font-weight: 600;
+    margin-bottom: 20px;
+  }
+
+  .settings-field-label {
+    display: block;
+    font-weight: 600;
+  }
+
+  .settings-field-hint {
+    color: rgba(0, 0, 0, 0.45);
+    font-size: 12px;
+    margin-bottom: 8px;
+  }
+
+  .settings-name-row {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    gap: 16px;
+
+    .settings-name-label label {
+      font-weight: 600;
+    }
+
+    .settings-hint {
+      color: rgba(0, 0, 0, 0.45);
+      font-size: 12px;
+      margin: 4px 0 0;
+    }
+
+    .settings-name-controls {
+      display: flex;
+      gap: 8px;
+      flex-shrink: 0;
+
+      input {
+        border-radius: 8px;
+      }
+
+      button {
+        border-radius: 8px;
+      }
+    }
+  }
+}
+
+::ng-deep .settings-general-card button {
+  border-radius: 8px;
+}
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 91d8acce94..3b40c12d09 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
@@ -18,7 +18,7 @@
  */
 
 import { ComponentFixture, TestBed } from "@angular/core/testing";
-import { ActivatedRoute } from "@angular/router";
+import { ActivatedRoute, Router } from "@angular/router";
 import { of, Subject, throwError } from "rxjs";
 import { NzModalService } from "ng-zorro-antd/modal";
 import { MarkdownService } from "ngx-markdown";
@@ -38,6 +38,7 @@ import { Dataset, DatasetVersion } from 
"../../../../../common/type/dataset";
 import { DashboardDataset } from 
"../../../../type/dashboard-dataset.interface";
 import { HttpErrorResponse } from "@angular/common/http";
 import { format } from "date-fns";
+import { USER_DATASET } from "../../../../../app-routing.constant";
 
 describe("DatasetDetailComponent upload queue", () => {
   let fixture: ComponentFixture<DatasetDetailComponent>;
@@ -429,6 +430,8 @@ describe("DatasetDetailComponent behavior", () => {
       updateDatasetDownloadable: vi.fn(() => of({})),
       updateDatasetCoverImage: vi.fn(() => of({})),
       updateDatasetDescription: vi.fn(() => of({})),
+      updateDatasetName: vi.fn(() => of({})),
+      deleteDatasets: vi.fn(() => of({})),
       deleteDatasetFile: vi.fn(() => of({})),
       getDatasetDiff: vi.fn(() => of([])),
       multipartUpload: vi.fn(() => of()),
@@ -1036,4 +1039,144 @@ describe("DatasetDetailComponent behavior", () => {
       expect(component.trackByTask(0, task)).toBe("owner/data/file.csv");
     });
   });
+
+  describe("onSaveDatasetName", () => {
+    it("seeds editedDatasetName from the loaded dataset name", () => {
+      datasetServiceStub.getDataset.mockReturnValue(
+        of(makeDashboardDataset({ dataset: makeDataset({ name: "seed-name" }) 
}))
+      );
+      createComponent();
+      component.did = 5;
+      component.retrieveDatasetInfo();
+
+      expect(component.editedDatasetName).toBe("seed-name");
+    });
+
+    it("persists a valid name unchanged and toasts success", () => {
+      datasetServiceStub.updateDatasetName.mockReturnValue(of({}));
+      createComponent();
+      component.did = 5;
+      // Mixed case, hyphen and underscore are all valid: the name must be 
saved
+      // verbatim, not rewritten.
+      component.editedDatasetName = "My-Cool_Dataset";
+
+      component.onSaveDatasetName();
+
+      expect(datasetServiceStub.updateDatasetName).toHaveBeenCalledWith(5, 
"My-Cool_Dataset");
+      expect(component.datasetName).toBe("My-Cool_Dataset");
+      expect(component.editedDatasetName).toBe("My-Cool_Dataset");
+      expect(notificationServiceStub.success).toHaveBeenCalledWith("Dataset 
name updated to 'My-Cool_Dataset'");
+    });
+
+    it("rejects an invalid name with a validation error and does not call the 
rename API", () => {
+      createComponent();
+      component.did = 5;
+      component.datasetName = "original";
+      component.editedDatasetName = "My Cool Dataset"; // spaces are not 
allowed
+
+      component.onSaveDatasetName();
+
+      expect(datasetServiceStub.updateDatasetName).not.toHaveBeenCalled();
+      expect(component.datasetName).toBe("original");
+      expect(notificationServiceStub.error).toHaveBeenCalledWith(
+        "Invalid dataset name: only letters, numbers, underscores, and hyphens 
are allowed (max 128 characters)"
+      );
+    });
+
+    it("toasts an error and leaves the name unchanged when the rename fails", 
() => {
+      datasetServiceStub.updateDatasetName.mockReturnValue(throwError(() => 
new Error("boom")));
+      createComponent();
+      component.did = 5;
+      component.datasetName = "original";
+      component.editedDatasetName = "new-name";
+
+      component.onSaveDatasetName();
+
+      expect(component.datasetName).toBe("original");
+      expect(notificationServiceStub.error).toHaveBeenCalledWith("boom");
+    });
+
+    it("does nothing when there is no did", () => {
+      createComponent();
+      component.did = undefined;
+      component.editedDatasetName = "whatever";
+
+      component.onSaveDatasetName();
+
+      expect(datasetServiceStub.updateDatasetName).not.toHaveBeenCalled();
+    });
+  });
+
+  describe("onDeleteDataset", () => {
+    it("deletes the dataset, toasts success and navigates back to the dataset 
list", () => {
+      datasetServiceStub.deleteDatasets.mockReturnValue(of({}));
+      createComponent();
+      const navigateSpy = vi.spyOn(TestBed.inject(Router), 
"navigate").mockResolvedValue(true);
+      component.did = 5;
+      component.datasetName = "DS";
+
+      component.onDeleteDataset();
+
+      expect(datasetServiceStub.deleteDatasets).toHaveBeenCalledWith(5);
+      expect(notificationServiceStub.success).toHaveBeenCalledWith("Dataset DS 
was deleted");
+      expect(navigateSpy).toHaveBeenCalledWith([USER_DATASET]);
+    });
+
+    it("toasts an error and does not navigate when the deletion fails", () => {
+      datasetServiceStub.deleteDatasets.mockReturnValue(throwError(() => new 
Error("boom")));
+      createComponent();
+      const navigateSpy = vi.spyOn(TestBed.inject(Router), 
"navigate").mockResolvedValue(true);
+      component.did = 5;
+
+      component.onDeleteDataset();
+
+      expect(notificationServiceStub.error).toHaveBeenCalledWith("boom");
+      expect(navigateSpy).not.toHaveBeenCalled();
+    });
+
+    it("does nothing when there is no did", () => {
+      createComponent();
+      const navigateSpy = vi.spyOn(TestBed.inject(Router), 
"navigate").mockResolvedValue(true);
+      component.did = undefined;
+
+      component.onDeleteDataset();
+
+      expect(datasetServiceStub.deleteDatasets).not.toHaveBeenCalled();
+      expect(navigateSpy).not.toHaveBeenCalled();
+    });
+  });
+
+  describe("delete button disabled state", () => {
+    // The Settings tab (and its Delete card) only render for WRITE access; the
+    // delete button itself is owner-only, mirroring the Downloadable switch's
+    // [nzDisabled]="!isOwner". Renders WRITE access with the given ownership,
+    // activates the (inactive) Settings tab so its pane is in the DOM, then
+    // returns the delete button element.
+    const renderDeleteButton = (isOwner: boolean): HTMLButtonElement => {
+      datasetServiceStub.getDataset.mockReturnValue(of(makeDashboardDataset({ 
accessPrivilege: "WRITE", isOwner })));
+      createComponent();
+      fixture.detectChanges();
+
+      const tabButtons: NodeListOf<HTMLElement> = 
fixture.nativeElement.querySelectorAll(".ant-tabs-tab-btn");
+      const settingsTab = Array.from(tabButtons).find(tab => 
tab.textContent?.includes("Settings"));
+      settingsTab?.click();
+      fixture.detectChanges();
+
+      return fixture.nativeElement.querySelector('button[title="Delete"]') as 
HTMLButtonElement;
+    };
+
+    it("disables the delete button for a non-owner with write access", () => {
+      const button = renderDeleteButton(false);
+
+      expect(button).toBeTruthy();
+      expect(button.disabled).toBe(true);
+    });
+
+    it("enables the delete button for the owner", () => {
+      const button = renderDeleteButton(true);
+
+      expect(button).toBeTruthy();
+      expect(button.disabled).toBe(false);
+    });
+  });
 });
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 bdef5e0168..3581459670 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
@@ -18,9 +18,15 @@
  */
 
 import { Component, EventEmitter, OnInit, Output, ViewChild } from 
"@angular/core";
-import { ActivatedRoute } from "@angular/router";
+import { ActivatedRoute, Router } from "@angular/router";
+import { USER_DATASET } from "../../../../../app-routing.constant";
+import { extractErrorMessage } from "../../../../../common/util/error";
 import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
-import { DatasetService, MultipartUploadProgress } from 
"../../../../service/user/dataset/dataset.service";
+import {
+  DatasetService,
+  MultipartUploadProgress,
+  validateDatasetName,
+} from "../../../../service/user/dataset/dataset.service";
 import { NzResizeEvent, NzResizableDirective, NzResizeHandleComponent } from 
"ng-zorro-antd/resizable";
 import {
   DatasetFileNode,
@@ -51,7 +57,7 @@ import { ɵNzTransitionPatchDirective } from 
"ng-zorro-antd/core/transition-patc
 import { NzIconDirective } from "ng-zorro-antd/icon";
 import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space";
 import { NzButtonComponent } from "ng-zorro-antd/button";
-import { NzPopoverDirective } from "ng-zorro-antd/popover";
+import { NzPopconfirmDirective } from "ng-zorro-antd/popconfirm";
 import { NzSwitchComponent } from "ng-zorro-antd/switch";
 import { FormsModule } from "@angular/forms";
 import { MarkdownDescriptionComponent } from 
"../../markdown-description/markdown-description.component";
@@ -68,6 +74,7 @@ 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;
@@ -88,7 +95,7 @@ export const ABORT_RETRY_BACKOFF_BASE_MS = 100;
     NzIconDirective,
     NzSpaceCompactItemDirective,
     NzButtonComponent,
-    NzPopoverDirective,
+    NzPopconfirmDirective,
     NzSwitchComponent,
     FormsModule,
     MarkdownDescriptionComponent,
@@ -114,11 +121,14 @@ export const ABORT_RETRY_BACKOFF_BASE_MS = 100;
     CdkVirtualScrollViewport,
     CdkFixedSizeVirtualScroll,
     CdkVirtualForOf,
+    NzTabsComponent,
+    NzTabComponent,
   ],
 })
 export class DatasetDetailComponent implements OnInit {
   public did: number | undefined;
   public datasetName: string = "";
+  public editedDatasetName: string = "";
   public datasetDescription: string = "";
   public datasetCreationTime: string = "";
   public datasetCreationTimeTooltip: string = "";
@@ -194,6 +204,7 @@ export class DatasetDetailComponent implements OnInit {
 
   constructor(
     private route: ActivatedRoute,
+    private router: Router,
     private modalService: NzModalService,
     private datasetService: DatasetService,
     private notificationService: NotificationService,
@@ -359,6 +370,7 @@ export class DatasetDetailComponent implements OnInit {
         .subscribe(dashboardDataset => {
           const dataset = dashboardDataset.dataset;
           this.datasetName = dataset.name;
+          this.editedDatasetName = dataset.name;
           this.datasetDescription = dataset.description;
           this.userDatasetAccessLevel = dashboardDataset.accessPrivilege;
           this.datasetIsPublic = dataset.isPublic;
@@ -917,6 +929,52 @@ export class DatasetDetailComponent implements OnInit {
       });
   }
 
+  onSaveDatasetName(): void {
+    if (!this.did) {
+      return;
+    }
+    // Reject invalid names outright instead of silently rewriting them, 
matching
+    // the shared validation used by the other rename entry points (PR #6426).
+    const name = this.editedDatasetName;
+    const nameError = validateDatasetName(name);
+    if (nameError) {
+      this.notificationService.error(nameError);
+      return;
+    }
+
+    this.datasetService
+      .updateDatasetName(this.did, name)
+      .pipe(untilDestroyed(this))
+      .subscribe({
+        next: () => {
+          this.datasetName = name;
+          this.editedDatasetName = name;
+          this.notificationService.success(`Dataset name updated to 
'${name}'`);
+        },
+        error: (err: unknown) => {
+          this.notificationService.error(extractErrorMessage(err));
+        },
+      });
+  }
+
+  onDeleteDataset(): void {
+    if (!this.did) {
+      return;
+    }
+    this.datasetService
+      .deleteDatasets(this.did)
+      .pipe(untilDestroyed(this))
+      .subscribe({
+        next: () => {
+          this.notificationService.success(`Dataset ${this.datasetName} was 
deleted`);
+          this.router.navigate([USER_DATASET]);
+        },
+        error: (err: unknown) => {
+          this.notificationService.error(extractErrorMessage(err));
+        },
+      });
+  }
+
   async copyCurrentFilePath(): Promise<void> {
     if (!this.currentDisplayedFileName) {
       return;

Reply via email to