Skip to content

Commit fb16000

Browse files
committed
feat: improve container file browser path jump
1 parent 42f6e9a commit fb16000

2 files changed

Lines changed: 103 additions & 16 deletions

File tree

agent/app/service/container.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,7 +1189,7 @@ func (u *ContainerService) ListContainerFiles(req dto.ContainerFileReq) ([]dto.C
11891189
ctx := context.Background()
11901190
stat, err := cli.ContainerStatPath(ctx, req.ContainerID, req.Path)
11911191
if err != nil {
1192-
return nil, err
1192+
return nil, normalizeContainerFileError(err)
11931193
}
11941194
isDir := stat.Mode.IsDir()
11951195
isLink := stat.Mode&os.ModeSymlink != 0
@@ -1205,7 +1205,7 @@ func (u *ContainerService) ListContainerFiles(req dto.ContainerFileReq) ([]dto.C
12051205

12061206
output, err := runContainerCommand(cli, req.ContainerID, []string{"ls", "-1A", "--", req.Path})
12071207
if err != nil {
1208-
return nil, err
1208+
return nil, normalizeContainerFileError(err)
12091209
}
12101210
lines := strings.Split(strings.TrimSpace(output), "\n")
12111211
files := make([]dto.ContainerFileInfo, 0, len(lines))
@@ -1348,7 +1348,7 @@ func (u *ContainerService) GetContainerFileContent(req dto.ContainerFileReq) (*d
13481348

13491349
stat, err := cli.ContainerStatPath(context.Background(), req.ContainerID, req.Path)
13501350
if err != nil {
1351-
return nil, err
1351+
return nil, normalizeContainerFileError(err)
13521352
}
13531353
if stat.Mode.IsDir() {
13541354
return nil, fmt.Errorf("path %s is directory", req.Path)
@@ -1395,7 +1395,7 @@ func (u *ContainerService) GetContainerFileSize(req dto.ContainerFileReq) (int64
13951395

13961396
stat, err := cli.ContainerStatPath(context.Background(), req.ContainerID, req.Path)
13971397
if err != nil {
1398-
return 0, err
1398+
return 0, normalizeContainerFileError(err)
13991399
}
14001400
if !stat.Mode.IsDir() {
14011401
return stat.Size, nil
@@ -1428,7 +1428,7 @@ func (u *ContainerService) DownloadContainerFile(req dto.ContainerFileReq) (io.R
14281428
stat, err := cli.ContainerStatPath(ctx, req.ContainerID, req.Path)
14291429
if err != nil {
14301430
_ = cli.Close()
1431-
return nil, "", "", err
1431+
return nil, "", "", normalizeContainerFileError(err)
14321432
}
14331433

14341434
fileName := stat.Name
@@ -1474,6 +1474,17 @@ func (u *ContainerService) DownloadContainerFile(req dto.ContainerFileReq) (io.R
14741474
}, fileName, "application/octet-stream", nil
14751475
}
14761476

1477+
func normalizeContainerFileError(err error) error {
1478+
if err == nil {
1479+
return nil
1480+
}
1481+
message := strings.ToLower(err.Error())
1482+
if strings.Contains(message, "no such file or directory") || strings.Contains(message, "not found") {
1483+
return buserr.New("ErrPathNotFound")
1484+
}
1485+
return err
1486+
}
1487+
14771488
func runContainerCommand(cli *client.Client, containerID string, command []string) (string, error) {
14781489
raw, err := runContainerCommandRaw(cli, containerID, command)
14791490
if err != nil {

frontend/src/views/container/container/file-browser/index.vue

Lines changed: 87 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,30 @@
88
@close="visible = false"
99
>
1010
<template #content>
11-
<el-form label-position="top">
11+
<el-form label-position="top" @submit.prevent>
1212
<el-form-item>
13-
<div class="path-breadcrumb">
13+
<div v-if="!pathEditing" class="path-breadcrumb" @click="enablePathEditing">
1414
<el-breadcrumb separator="/">
1515
<el-breadcrumb-item>
16-
<el-link type="primary" @click="navigateToPath('/')">
16+
<el-link type="primary" @click.stop="navigateToPath('/')">
1717
<el-icon><HomeFilled /></el-icon>
1818
</el-link>
1919
</el-breadcrumb-item>
2020
<el-breadcrumb-item v-for="item in pathSegments" :key="item.path">
21-
<el-link type="primary" @click="navigateToPath(item.path)">
21+
<el-link type="primary" @click.stop="navigateToPath(item.path)">
2222
{{ item.name }}
2323
</el-link>
2424
</el-breadcrumb-item>
2525
</el-breadcrumb>
2626
</div>
27+
<el-input
28+
v-else
29+
ref="pathInputRef"
30+
v-model="pathInput"
31+
class="path-breadcrumb path-input"
32+
@blur="cancelPathEditing"
33+
@keyup.enter.prevent="submitPathInput"
34+
/>
2735
<el-upload ref="uploadRef" :auto-upload="false" :show-file-list="false" :on-change="onUploadChange">
2836
<el-button class="mt-2" :loading="uploading" type="primary" plain>
2937
{{ $t('commons.button.upload') }}
@@ -103,7 +111,7 @@
103111
</template>
104112

105113
<script lang="ts" setup>
106-
import { computed, ref } from 'vue';
114+
import { computed, nextTick, ref } from 'vue';
107115
import {
108116
deleteContainerFile,
109117
downloadContainerFile,
@@ -114,7 +122,7 @@ import {
114122
} from '@/api/modules/container';
115123
import { MsgError, MsgSuccess, MsgWarning } from '@/utils/message';
116124
import i18n from '@/lang';
117-
import { ElMessageBox, UploadFile, UploadInstance } from 'element-plus';
125+
import { ElInput, ElMessageBox, UploadFile, UploadInstance } from 'element-plus';
118126
import { Document, FolderOpened, HomeFilled } from '@element-plus/icons-vue';
119127
import { computeSize2 } from '@/utils/util';
120128
@@ -124,12 +132,15 @@ const containerID = ref('');
124132
const filePath = ref('/');
125133
const containerFiles = ref<any[]>([]);
126134
const uploadRef = ref<UploadInstance>();
135+
const pathInputRef = ref<InstanceType<typeof ElInput>>();
127136
const uploading = ref(false);
128137
const previewVisible = ref(false);
129138
const previewTitle = ref('');
130139
const previewContent = ref('');
131140
const previewTruncated = ref(false);
132141
const selectedRows = ref<any[]>([]);
142+
const pathEditing = ref(false);
143+
const pathInput = ref('/');
133144
const pathSegments = computed(() => {
134145
const parts = filePath.value.split('/').filter((item) => item);
135146
return parts.map((name, index) => ({
@@ -149,16 +160,18 @@ const acceptParams = async (params: DrawerProps): Promise<void> => {
149160
containerID.value = params.containerID;
150161
title.value = params.title;
151162
filePath.value = params.workingDir || '/';
163+
pathInput.value = filePath.value;
164+
pathEditing.value = false;
152165
await loadContainerFiles();
153166
};
154167
155-
const loadContainerFiles = async () => {
156-
if (!containerID.value || !filePath.value) {
157-
return;
168+
const fetchContainerFiles = async (path: string) => {
169+
if (!containerID.value || !path) {
170+
return false;
158171
}
159-
await listContainerFiles({
172+
return await listContainerFiles({
160173
containerID: containerID.value,
161-
path: filePath.value,
174+
path,
162175
})
163176
.then((res) => {
164177
containerFiles.value = (res.data || []).map((item) => ({
@@ -167,22 +180,80 @@ const loadContainerFiles = async () => {
167180
sizeLoading: false,
168181
}));
169182
selectedRows.value = [];
183+
return true;
170184
})
171185
.catch(() => {
172186
containerFiles.value = [];
187+
return false;
173188
});
174189
};
175190
191+
const loadContainerFiles = async () => {
192+
await fetchContainerFiles(filePath.value);
193+
};
194+
176195
const enterDir = async (path: string) => {
177196
filePath.value = path;
178197
await loadContainerFiles();
179198
};
180199
181200
const navigateToPath = async (path: string) => {
182201
filePath.value = path || '/';
202+
pathInput.value = filePath.value;
203+
pathEditing.value = false;
183204
await loadContainerFiles();
184205
};
185206
207+
const getParentPath = (path: string) => {
208+
if (!path || path === '/') {
209+
return '/';
210+
}
211+
const segments = path.split('/').filter((item) => item);
212+
if (segments.length <= 1) {
213+
return '/';
214+
}
215+
return '/' + segments.slice(0, -1).join('/');
216+
};
217+
218+
const enablePathEditing = async () => {
219+
pathInput.value = filePath.value || '/';
220+
pathEditing.value = true;
221+
await nextTick();
222+
pathInputRef.value?.focus();
223+
};
224+
225+
const cancelPathEditing = () => {
226+
pathEditing.value = false;
227+
pathInput.value = filePath.value || '/';
228+
};
229+
230+
const submitPathInput = async () => {
231+
let targetPath = (pathInput.value || '').trim();
232+
if (!targetPath) {
233+
targetPath = '/';
234+
}
235+
if (!targetPath.startsWith('/')) {
236+
targetPath = '/' + targetPath;
237+
}
238+
let currentPath = targetPath;
239+
while (true) {
240+
const success = await fetchContainerFiles(currentPath);
241+
if (success) {
242+
filePath.value = currentPath;
243+
pathInput.value = currentPath;
244+
pathEditing.value = false;
245+
return;
246+
}
247+
if (currentPath === '/') {
248+
filePath.value = '/';
249+
pathInput.value = '/';
250+
pathEditing.value = false;
251+
return;
252+
}
253+
currentPath = getParentPath(currentPath);
254+
}
255+
};
256+
186257
const onUploadChange = async (uploadFile: UploadFile) => {
187258
if (!uploadFile.raw) {
188259
return;
@@ -331,6 +402,11 @@ defineExpose({
331402
border: 1px solid var(--el-border-color);
332403
border-radius: 4px;
333404
overflow-x: auto;
405+
cursor: text;
406+
}
407+
408+
.path-input {
409+
overflow: visible;
334410
}
335411
336412
.preview-content {

0 commit comments

Comments
 (0)