- {{ path }}
-
-
Copied!
+
+
+ {{ path }}
+
+ {{ path }}
+
Not Available
diff --git a/frontend/src/composables/String.js b/frontend/src/composables/String.js
index 04f41b4739..c0a3edf7fb 100644
--- a/frontend/src/composables/String.js
+++ b/frontend/src/composables/String.js
@@ -20,3 +20,14 @@ export const dateToSlug = (date) => {
return `${year}-${month}-${day}_${hours}-${minutes}`
}
+
+// utility function to remove leading and trailing slashes
+export const removeSlashes = (str, leading = true, trailing = true) => {
+ if (leading && str.startsWith('/')) {
+ str = str.slice(1)
+ }
+ if (trailing && str.endsWith('/')) {
+ str = str.slice(0, -1)
+ }
+ return str
+}
diff --git a/frontend/src/pages/instance/Assets.vue b/frontend/src/pages/instance/Assets.vue
index 042bb4c6b6..fa5ed234de 100644
--- a/frontend/src/pages/instance/Assets.vue
+++ b/frontend/src/pages/instance/Assets.vue
@@ -3,24 +3,22 @@
-
-
-
-
-
- /
-
-
-
-
- /
-
- /
-
+
+
2.8.0 in order to use this feature.',
instanceSuspendedMessage: 'The instance must be running to access its assets.'
}
},
computed: {
+ currentDirectory () {
+ if (this.breadcrumbs.length) {
+ return this.breadcrumbs[this.breadcrumbs.length - 1]
+ }
+ return null
+ },
launcherSatisfiesVersion () {
if (!this.isInstanceRunning) {
return true
@@ -85,15 +87,26 @@ export default {
},
isInstanceRunning () {
return this.instance?.meta?.state === 'running'
+ },
+ sortedFiles () {
+ const files = this.files.filter(file => file.type === 'file').sort()
+ const folders = this.files.filter(file => file.type === 'directory').sort()
+
+ return [...folders, ...files]
}
},
watch: {
isInstanceRunning (newState, oldState) {
if (newState && !oldState) {
- this.loadContents()
+ this.loadContents(this.breadcrumbs, true)
} else {
this.files = []
}
+ },
+ currentDirectory (currentDirectory, previousDirectory) {
+ if (currentDirectory?.name !== previousDirectory?.name) {
+ this.loadContents()
+ }
}
},
mounted () {
@@ -104,13 +117,19 @@ export default {
this.loadContents()
},
methods: {
- loadContents () {
+ loadContents (breadcrumbs = [], reloadDirectory = false) {
if (this.isFeatureEnabled) {
- const breadcrumbs = this.breadcrumbs.join('/')
- const path = breadcrumbs + (breadcrumbs.length > 0 ? '/' : '') + (this.currentDirectory.name || '')
- AssetsAPI.getFiles(this.instance.id, path)
- .then(files => {
- this.files = files
+ if (breadcrumbs.length === 0) {
+ breadcrumbs = this.breadcrumbs
+ }
+
+ const filepath = breadcrumbs.map(crumb => crumb.name).join('/')
+ return AssetsAPI.getFiles(this.instance.id, filepath)
+ .then(payload => {
+ this.files = payload.files
+ if (payload.folder && reloadDirectory) {
+ this.breadcrumbs[this.breadcrumbs.length - 1] = payload.folder
+ }
})
.catch(error => {
console.error(error)
@@ -118,21 +137,40 @@ export default {
}
},
changeDirectory (dir) {
- this.breadcrumbs.push(this.currentDirectory.name || '')
- this.currentDirectory.name = dir.name
- this.loadContents()
+ if (dir.name) {
+ this.breadcrumbs.push(dir)
+ }
},
- breadcrumbClicked ($index) {
- this.currentDirectory.name = this.breadcrumbs[$index] || ''
- this.breadcrumbs = this.breadcrumbs.slice(0, $index)
- this.loadContents()
+ goBack () {
+ this.breadcrumbs.pop()
+ if (this.breadcrumbs.length === 0) {
+ this.changeDirectory(null)
+ } else {
+ this.changeDirectory(this.breadcrumbs.pop())
+ }
+ },
+ onVisibilitySelected (payload) {
+ const pwd = this.breadcrumbs
+ .map(crumb => crumb.name)
+ .join('/')
+ .replace('//', '/')
+
+ AssetsAPI.updateVisibility(
+ this.instance.id,
+ pwd,
+ payload.visibility,
+ payload.path
+ )
+ .then((res) => this.loadContents())
+ .then((res) => Alerts.emit('Instance settings successfully updated. Restart the instance to apply the changes.', 'confirmation', 6000))
+ .catch(err => console.warn(err))
}
}
}
diff --git a/frontend/src/pages/instance/components/DashboardLink.vue b/frontend/src/pages/instance/components/DashboardLink.vue
index 6b26f1547e..9e9cff8cb8 100644
--- a/frontend/src/pages/instance/components/DashboardLink.vue
+++ b/frontend/src/pages/instance/components/DashboardLink.vue
@@ -25,16 +25,7 @@
import { ChartPieIcon } from '@heroicons/vue/outline'
-// utility function to remove leading and trailing slashes
-const removeSlashes = (str, leading = true, trailing = true) => {
- if (leading && str.startsWith('/')) {
- str = str.slice(1)
- }
- if (trailing && str.endsWith('/')) {
- str = str.slice(0, -1)
- }
- return str
-}
+import { removeSlashes } from '../../../composables/String.js'
export default {
name: 'DashboardLink',
diff --git a/frontend/src/pages/instance/components/FolderBreadcrumbs.vue b/frontend/src/pages/instance/components/FolderBreadcrumbs.vue
new file mode 100644
index 0000000000..cdf7a032cb
--- /dev/null
+++ b/frontend/src/pages/instance/components/FolderBreadcrumbs.vue
@@ -0,0 +1,175 @@
+
+
+
+
+
+
+
diff --git a/test/e2e/frontend/cypress/tests-ee/instances/assets.spec.js b/test/e2e/frontend/cypress/tests-ee/instances/assets.spec.js
index 47d195a4af..df356859f5 100644
--- a/test/e2e/frontend/cypress/tests-ee/instances/assets.spec.js
+++ b/test/e2e/frontend/cypress/tests-ee/instances/assets.spec.js
@@ -15,7 +15,7 @@ function interceptTeamFeature () {
.as('getTeam')
}
-function interceptInstanceMeta () {
+function interceptInstanceMeta (body = {}) {
cy.intercept('GET', '/api/*/projects/*', (req) => req.reply(res => {
// this.instance?.meta?.versions?.launcher
res.body = {
@@ -26,7 +26,8 @@ function interceptInstanceMeta () {
launcher: '2.8.0'
}
}
- }
+ },
+ ...body
}
return res
})).as('getInstanceStatus')
@@ -340,9 +341,237 @@ describe('FlowForge - Instance - Assets', () => {
cy.get('[data-el="folder-breadcrumbs"]').contains('hello_world')
- cy.get('[data-el="folder-breadcrumbs"]').contains('Storage').click()
+ cy.get('[data-action="navigate-back"]').click()
cy.get('[data-el="folder-breadcrumbs"]').should('not.contain', 'hello_world')
+
+ cy.get('[data-el="files-table"] table tbody').contains('hello_world')
+ })
+ })
+
+ describe('the Folder Breadcrumbs', () => {
+ beforeEach(() => {
+ interceptTeamFeature()
+ interceptInstanceMeta()
+
+ navigateToProject('BTeam', 'instance-2-1', 'assets')
+ })
+
+ it('displays a disabled visibility selector on the default root folder and correct folder path', () => {
+ interceptFiles([
+ { name: 'hello_world', type: 'directory', lastModified: new Date() }
+ ])
+ cy.wait('@getFiles')
+
+ cy.intercept('GET', '/api/*/projects/*/files/_/hello_world', ({
+ meta: { },
+ files: [],
+ count: 0
+ })).as('getNestedFiles')
+
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="visibility-selector"]')
+ .click()
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="visibility-selector"] .ff-dropdown-options')
+ .should('not.be.visible')
+ cy.get('[data-el="folder-breadcrumbs"]').contains('Storage')
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="ff-data-cell"]:nth-child(4) .path').should('be.empty')
+ })
+
+ it('displays an enabled visibility selector on nested folders and correct folder path', () => {
+ interceptFiles([
+ { name: 'hello_world', type: 'directory', lastModified: new Date() }
+ ])
+ cy.intercept('GET', '/api/*/projects/*/files/_/hello_world', ({
+ meta: { },
+ files: [],
+ count: 0
+ })).as('getNestedFiles')
+ cy.wait('@getFiles')
+
+ cy.intercept('GET', '/api/*/projects/*/files/_/hello_world', ({
+ meta: { },
+ files: [],
+ count: 0
+ })).as('getNestedFiles')
+
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="visibility-selector"]')
+ .click()
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="visibility-selector"] .ff-dropdown-options')
+ .should('not.be.visible')
+
+ cy.get('[data-el="folder-breadcrumbs"]').contains('Storage')
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="ff-data-cell"]:nth-child(4) .path').should('be.empty')
+
+ cy.get('[data-el="files-table"] table tbody tr').contains('hello_world').click()
+ cy.wait('@getNestedFiles')
+
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="visibility-selector"]')
+ .click()
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="visibility-selector"] .ff-dropdown-options')
+ .should('be.visible')
+
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="ff-data-cell"]:nth-child(2)').contains('hello_world')
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="ff-data-cell"]:nth-child(4)').contains('hello_world/')
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="ff-data-cell"]:nth-child(5)').contains('Not Available')
+ })
+
+ it('prevents users to set private visibility when the folder is private', () => {
+ const spy = cy.spy().as('updateVisibilityCall')
+ cy.intercept('PUT', '/api/*/projects/*/files/_/hello_world', spy)
+ interceptFiles([
+ { name: 'hello_world', type: 'directory', lastModified: new Date() }
+ ])
+ cy.intercept('GET', '/api/*/projects/*/files/_/hello_world', ({
+ meta: { },
+ files: [],
+ count: 0
+ })).as('getNestedFiles')
+ cy.wait('@getFiles')
+
+ cy.get('[data-el="files-table"] table tbody tr').contains('hello_world').click()
+ cy.wait('@getNestedFiles')
+
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="visibility-selector"]')
+ .click()
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="visibility-selector"] .ff-dropdown-options')
+ .should('be.visible')
+ .within(() => {
+ cy.get('[data-action="select-private"]').click()
+ cy.get('@updateVisibilityCall').should('not.have.been.called')
+ })
+ })
+
+ it('allows users to set private visibility when the folder is public', () => {
+ const spy = cy.spy().as('updateVisibilityCall')
+ cy.intercept('PUT', '/api/*/projects/*/files/_/hello_world', spy)
+ interceptFiles([
+ {
+ name: 'hello_world',
+ type: 'directory',
+ lastModified: new Date(),
+ share: {
+ root: 'hello_public'
+ }
+ }
+ ])
+ cy.intercept('GET', '/api/*/projects/*/files/_/hello_world', ({
+ meta: { },
+ files: [],
+ count: 0
+ })).as('getNestedFiles')
+ cy.wait('@getFiles')
+
+ cy.get('[data-el="files-table"] table tbody tr').contains('hello_world').click()
+ cy.wait('@getNestedFiles')
+
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="visibility-selector"]')
+ .click()
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="visibility-selector"] .ff-dropdown-options')
+ .should('be.visible')
+ .within(() => {
+ cy.get('[data-action="select-private"]').click()
+ cy.get('@updateVisibilityCall').should('have.been.called', 1)
+ })
+ })
+
+ it('allows users to set public visibility when the directory is private', () => {
+ cy.intercept('PUT', '/api/*/projects/*/files/_/hello_world', { }).as('updateVisibility')
+ interceptFiles([
+ {
+ name: 'hello_world',
+ type: 'directory',
+ lastModified: new Date()
+ }
+ ])
+ cy.intercept('GET', '/api/*/projects/*/files/_/hello_world', ({
+ meta: { },
+ files: [],
+ count: 0
+ })).as('getNestedFiles')
+ cy.wait('@getFiles')
+
+ cy.get('[data-el="files-table"] table tbody tr').contains('hello_world').click()
+ cy.wait('@getNestedFiles')
+
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="visibility-selector"]')
+ .click()
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="visibility-selector"] .ff-dropdown-options')
+ .should('be.visible')
+ .within(() => {
+ cy.get('[data-action="select-public"]').click()
+ })
+
+ cy.get('[data-el="select-static-path-dialog"]')
+ .should('be.visible')
+ cy.get('[data-el="select-static-path-dialog"] input').type('public_path')
+ cy.get('[data-el="select-static-path-dialog"]').within(() => {
+ cy.get('[data-action="dialog-confirm"]').click()
+ })
+
+ cy.wait('@updateVisibility')
+ cy.get('[data-el="notification-alert"]').should('be.visible')
+ cy.get('[data-el="notification-alert"]').contains('Instance settings successfully updated. Restart the instance to apply the changes.')
+ })
+
+ it('allows users to set public visibility when the directory is public', () => {
+ cy.intercept('PUT', '/api/*/projects/*/files/_/hello_world', { }).as('updateVisibility')
+ interceptFiles([
+ {
+ name: 'hello_world',
+ type: 'directory',
+ lastModified: new Date(),
+ share: {
+ root: 'public-path'
+ }
+ }
+ ])
+ cy.intercept('GET', '/api/*/projects/*/files/_/hello_world', ({
+ meta: { },
+ files: [],
+ count: 0
+ })).as('getNestedFiles')
+ cy.wait('@getFiles')
+
+ cy.get('[data-el="files-table"] table tbody tr').contains('hello_world').click()
+ cy.wait('@getNestedFiles')
+
+ cy.get('[data-el="folder-breadcrumbs"]').contains('public-path/')
+
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="visibility-selector"]')
+ .click()
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="visibility-selector"] .ff-dropdown-options')
+ .should('be.visible')
+ .within(() => {
+ cy.get('[data-action="select-public"]').click()
+ })
+
+ cy.get('[data-el="select-static-path-dialog"]')
+ .should('be.visible')
+ cy.get('[data-el="select-static-path-dialog"] input').type('public_path')
+ cy.get('[data-el="select-static-path-dialog"]').within(() => {
+ cy.get('[data-action="dialog-confirm"]').click()
+ })
+
+ cy.wait('@updateVisibility')
+ cy.get('[data-el="notification-alert"]').should('be.visible')
+ cy.get('[data-el="notification-alert"]').contains('Instance settings successfully updated. Restart the instance to apply the changes.')
+ })
+
+ it('is disabled when the instance state is not running', () => {
+ interceptInstanceMeta({ meta: { state: 'suspended' } })
+
+ cy.get('[data-el="status-badge-suspended"]').should('exist')
+ cy.get('[data-el="files-table"]').contains('The instance must be running to access its assets.')
+
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="visibility-selector"]').click()
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="visibility-selector"] .ff-dropdown-options')
+ .should('not.be.visible')
+
+ // checking folder path
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="ff-data-cell"]:nth-child(4)').contains('Not Available')
+
+ // checking Base URL
+ cy.get('[data-el="folder-breadcrumbs"] [data-el="ff-data-cell"]:nth-child(5)').contains('Not Available')
})
})
})