Implement Share Management UI (Phase 11 P0 #2)
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

Share Management Features:
- Shares.vue: Complete share CRUD interface
- Tauri commands: 5 share endpoints
- In-memory share storage (lazy_static)

UI Components:
- Share list table (name, path, protocol, users, permissions)
- Create share dialog (name, path, protocol, users, permissions)
- Edit share dialog (path, protocol, users, permissions)
- Delete share confirmation
- Test connection button

Tauri Commands:
- list_shares: List all shares
- create_share: Create share + create directory if needed
- update_share: Update share config
- delete_share: Remove share from list
- test_share_connection: Test share path exists

Supported Protocols:
- SMB/CIFS (default)
- SFTP
- WebDAV
- S3

Router:
- Added /shares route

Home.vue:
- Added Share Management card

Build:  Tauri + markbase-core
Tests: 495 markbase-core + 201 smb-server
This commit is contained in:
Warren
2026-06-24 05:16:24 +08:00
parent e07d17aee7
commit 103bb66924
6 changed files with 470 additions and 2 deletions

View File

@@ -8,6 +8,7 @@ import Health from '../views/Health.vue'
import Monitor from '../views/Monitor.vue'
import Backup from '../views/Backup.vue'
import Users from '../views/Users.vue'
import Shares from '../views/Shares.vue'
const routes = [
{
@@ -54,6 +55,11 @@ const routes = [
path: '/users',
name: 'Users',
component: Users
},
{
path: '/shares',
name: 'Shares',
component: Shares
}
]

View File

@@ -4,7 +4,7 @@ import { useRouter } from 'vue-router'
import { useAppStore } from '../stores/app'
import { invoke } from '@tauri-apps/api/tauri'
import { ElMessage } from 'element-plus'
import { Folder, Document, Upload, Clock, UserFilled } from '@element-plus/icons-vue'
import { Folder, Document, Upload, Clock, UserFilled, FolderOpened } from '@element-plus/icons-vue'
import { open } from '@tauri-apps/api/dialog'
const router = useRouter()
@@ -233,6 +233,14 @@ onMounted(async () => {
<p>Users and permissions</p>
</div>
</el-card>
<el-card class="management-card" @click="navigateTo('/shares')">
<div class="card-content">
<el-icon :size="40"><FolderOpened /></el-icon>
<h3>Share Management</h3>
<p>SMB/SFTP/WebDAV shares</p>
</div>
</el-card>
</div>
</el-col>
</el-row>

View File

@@ -0,0 +1,295 @@
<script setup>
import { ref, onMounted } from 'vue'
import { invoke } from '@tauri-apps/api/core'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
FolderOpened,
Plus,
Edit,
Delete,
Connection,
Network,
Document,
} from '@element-plus/icons-vue'
const shares = ref([])
const loading = ref(false)
const showCreateDialog = ref(false)
const showEditDialog = ref(false)
const currentShare = ref({
name: '',
path: '',
protocol: 'smb',
users: '',
permissions: 'rw',
})
const editingShare = ref(null)
const protocols = [
{ label: 'SMB/CIFS', value: 'smb' },
{ label: 'SFTP', value: 'sftp' },
{ label: 'WebDAV', value: 'webdav' },
{ label: 'S3', value: 's3' },
]
const permissionOptions = [
{ label: 'Read/Write', value: 'rw' },
{ label: 'Read Only', value: 'r' },
{ label: 'Write Only', value: 'w' },
{ label: 'No Access', value: 'none' },
]
const loadShares = async () => {
loading.value = true
try {
const list = await invoke('list_shares')
shares.value = list
} catch (error) {
ElMessage.error(`Failed to load shares: ${error}`)
} finally {
loading.value = false
}
}
const createShare = async () => {
if (!currentShare.value.name) {
ElMessage.warning('Please enter share name')
return
}
if (!currentShare.value.path) {
ElMessage.warning('Please enter path')
return
}
loading.value = true
try {
await invoke('create_share', {
name: currentShare.value.name,
path: currentShare.value.path,
protocol: currentShare.value.protocol,
users: currentShare.value.users.split(',').filter(u => u.trim()),
permissions: currentShare.value.permissions,
})
ElMessage.success(`Share '${currentShare.value.name}' created`)
showCreateDialog.value = false
currentShare.value = { name: '', path: '', protocol: 'smb', users: '', permissions: 'rw' }
await loadShares()
} catch (error) {
ElMessage.error(`Failed to create share: ${error}`)
} finally {
loading.value = false
}
}
const editShare = (share) => {
editingShare.value = { ...share, users: share.users.join(',') }
showEditDialog.value = true
}
const updateShare = async () => {
if (!editingShare.value.name) {
ElMessage.warning('Please enter share name')
return
}
loading.value = true
try {
await invoke('update_share', {
name: editingShare.value.name,
path: editingShare.value.path,
protocol: editingShare.value.protocol,
users: editingShare.value.users.split(',').filter(u => u.trim()),
permissions: editingShare.value.permissions,
})
ElMessage.success(`Share '${editingShare.value.name}' updated`)
showEditDialog.value = false
editingShare.value = null
await loadShares()
} catch (error) {
ElMessage.error(`Failed to update share: ${error}`)
} finally {
loading.value = false
}
}
const deleteShare = async (name) => {
try {
await ElMessageBox.confirm(
`Are you sure you want to delete share '${name}'?`,
'Delete Share',
{ type: 'warning' }
)
loading.value = true
await invoke('delete_share', { name })
ElMessage.success(`Share '${name}' deleted`)
await loadShares()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error(`Failed to delete share: ${error}`)
}
} finally {
loading.value = false
}
}
const testConnection = async (share) => {
loading.value = true
try {
const result = await invoke('test_share_connection', {
name: share.name,
protocol: share.protocol,
})
if (result.success) {
ElMessage.success(`Connection to '${share.name}' successful`)
} else {
ElMessage.error(`Connection failed: ${result.error}`)
}
} catch (error) {
ElMessage.error(`Test failed: ${error}`)
} finally {
loading.value = false
}
}
onMounted(async () => {
await loadShares()
})
</script>
<template>
<div class="shares-container">
<el-card>
<template #header>
<div class="card-header">
<span><el-icon><FolderOpened /></el-icon> Share Management</span>
<el-button type="primary" :icon="Plus" @click="showCreateDialog = true">
Create Share
</el-button>
</div>
</template>
<el-table :data="shares" v-loading="loading" style="width: 100%">
<el-table-column prop="name" label="Share Name" min-width="150">
<template #default="{ row }">
<span style="display: flex; align-items: center; gap: 8px;">
<el-icon><FolderOpened /></el-icon>
{{ row.name }}
</span>
</template>
</el-table-column>
<el-table-column prop="path" label="Path" min-width="200" />
<el-table-column prop="protocol" label="Protocol" width="100">
<template #default="{ row }">
<el-tag size="small">{{ row.protocol.toUpperCase() }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="permissions" label="Permissions" width="100">
<template #default="{ row }">
<el-tag :type="row.permissions === 'rw' ? 'success' : 'info'" size="small">
{{ row.permissions }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="users" label="Users" width="150">
<template #default="{ row }">
<span>{{ row.users.join(', ') }}</span>
</template>
</el-table-column>
<el-table-column label="Actions" width="200" fixed="right">
<template #default="{ row }">
<el-button-group>
<el-button size="small" :icon="Edit" @click="editShare(row)">
Edit
</el-button>
<el-button size="small" :icon="Connection" @click="testConnection(row)">
Test
</el-button>
<el-button size="small" type="danger" :icon="Delete" @click="deleteShare(row.name)">
Delete
</el-button>
</el-button-group>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- Create Share Dialog -->
<el-dialog v-model="showCreateDialog" title="Create Share" width="500px">
<el-form label-width="120px">
<el-form-item label="Share Name">
<el-input v-model="currentShare.name" placeholder="Enter share name" />
</el-form-item>
<el-form-item label="Path">
<el-input v-model="currentShare.path" placeholder="/data/share" />
</el-form-item>
<el-form-item label="Protocol">
<el-select v-model="currentShare.protocol" style="width: 100%;">
<el-option v-for="p in protocols" :key="p.value" :label="p.label" :value="p.value" />
</el-select>
</el-form-item>
<el-form-item label="Users">
<el-input v-model="currentShare.users" placeholder="user1,user2,user3" />
<span style="color: #909399; font-size: 12px;">Comma-separated user list</span>
</el-form-item>
<el-form-item label="Permissions">
<el-select v-model="currentShare.permissions" style="width: 100%;">
<el-option v-for="p in permissionOptions" :key="p.value" :label="p.label" :value="p.value" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showCreateDialog = false">Cancel</el-button>
<el-button type="primary" @click="createShare" :loading="loading">Create</el-button>
</template>
</el-dialog>
<!-- Edit Share Dialog -->
<el-dialog v-model="showEditDialog" title="Edit Share" width="500px">
<el-form label-width="120px">
<el-form-item label="Share Name">
<el-input v-model="editingShare.name" disabled />
</el-form-item>
<el-form-item label="Path">
<el-input v-model="editingShare.path" />
</el-form-item>
<el-form-item label="Protocol">
<el-select v-model="editingShare.protocol" style="width: 100%;">
<el-option v-for="p in protocols" :key="p.value" :label="p.label" :value="p.value" />
</el-select>
</el-form-item>
<el-form-item label="Users">
<el-input v-model="editingShare.users" placeholder="user1,user2,user3" />
<span style="color: #909399; font-size: 12px;">Comma-separated user list</span>
</el-form-item>
<el-form-item label="Permissions">
<el-select v-model="editingShare.permissions" style="width: 100%;">
<el-option v-for="p in permissionOptions" :key="p.value" :label="p.label" :value="p.value" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showEditDialog = false">Cancel</el-button>
<el-button type="primary" @click="updateShare" :loading="loading">Update</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.shares-container {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-header span {
display: flex;
align-items: center;
gap: 8px;
}
</style>