Skip to content

Commit

Permalink
feat: hide vfolder sharing buttons when invite-others permission is d…
Browse files Browse the repository at this point in the history
…isabled (#1642)

* add: composite graphQL query to receive allowed_vfolder_hosts at once

* refactor: hide vfolder sharing related icon-button when `invite-other` permission is not allowed in any policy associated to the current user

* refactor: getAllowedVFolderHostsByCurrentUserInfo

* refactor: query installed images only in checking filebrowser supported images

The previous logic fetches every image including not-installed ones, and
then filter installed ones. This takes quite some time if there are
several hundreds of registered images. Since the image list query API
already supports fetching the installed ones only, we don't need to
fetch not-installed images.

* fix: remove console error after creating a data folder

The error below was raised to update the auto mount folder tab since
the `this.folderListGrid` is `null` in the page:

```
Unhandled Promise Rejection: TypeError: null is not an object
(evaluating 'this.folderListGrid.clearCache')
```

* refactor: more responsiveness using async calls in viewStateChanged

* refactor: add element ID for preferred app port config box in app launcher

---------

Co-authored-by: Jonghyun Park <[email protected]>
  • Loading branch information
lizable and adrysn authored Mar 18, 2023
1 parent 2b14234 commit d4b2023
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 38 deletions.
2 changes: 1 addition & 1 deletion src/components/backend-ai-app-launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1137,7 +1137,7 @@ export default class BackendAiAppLauncher extends BackendAIPage {
.helper="(${_t('session.CommaSeparated')})"></mwc-textfield>
</div>
`}
<div class="horizontal layout center">
<div id="preferred-app-port-config-box" style="display:none" class="horizontal layout center">
<mwc-checkbox id="chk-preferred-port" style="margin-right:0.5em;"></mwc-checkbox>
${_t('session.TryPreferredPort')}
<mwc-textfield id="app-port" type="number" no-label-float value="10250"
Expand Down
24 changes: 14 additions & 10 deletions src/components/backend-ai-data-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export default class BackendAIData extends BackendAIPage {
@property({type: Number}) capacity;
@property({type: String}) cloneFolderName = '';
@property({type: Array}) quotaSupportStorageBackends = ['xfs', 'weka', 'spectrumscale'];
@property({type: Object}) storageProxyInfo = Object();
@property({type: Object}) volumeInfo = Object();
@property({type: String}) folderType = 'user';
@query('#add-folder-name') addFolderNameInput!: TextField;
@query('#clone-folder-name') cloneFolderNameInput!: TextField;
Expand Down Expand Up @@ -580,10 +580,10 @@ export default class BackendAIData extends BackendAIPage {
};
if (typeof globalThis.backendaiclient === 'undefined' || globalThis.backendaiclient === null || globalThis.backendaiclient.ready === false) {
document.addEventListener('backend-ai-connected', () => {
this._getStorageProxyBackendInformation();
this._getVolumeInformation();
}, true);
} else { // already connected
this._getStorageProxyBackendInformation();
this._getVolumeInformation();
}
document.addEventListener('backend-ai-folder-list-changed', () => {
// this.shadowRoot.querySelector('#storage-status').updateChart();
Expand Down Expand Up @@ -618,7 +618,7 @@ export default class BackendAIData extends BackendAIPage {
this.usageModes.push('Model');
}
this.apiMajorVersion = globalThis.backendaiclient.APIMajorVersion;
this._getStorageProxyBackendInformation();
this._getVolumeInformation();
if (globalThis.backendaiclient.isAPIVersionCompatibleWith('v4.20191215')) {
this._vfolderInnatePermissionSupport = true;
}
Expand All @@ -638,14 +638,18 @@ export default class BackendAIData extends BackendAIPage {
}
}

/** *
private async _getCurrentKeypairResourcePolicy() {
const accessKey = globalThis.backendaiclient._config.accessKey;
const res = await globalThis.backendaiclient.keypair.info(accessKey, ['resource_policy']);
return res.keypair.resource_policy;
}

/**
* create Storage Doughnut Chart
*
*/
async _createStorageChart() {
const accessKey = globalThis.backendaiclient._config.accessKey;
const res = await globalThis.backendaiclient.keypair.info(accessKey, ['resource_policy']);
const policyName = res.keypair.resource_policy;
const policyName = await this._getCurrentKeypairResourcePolicy();
const resource_policy = await globalThis.backendaiclient.resourcePolicy.get(policyName, ['max_vfolder_count']);
const max_vfolder_count = resource_policy.keypair_resource_policy.max_vfolder_count;
const groupId = globalThis.backendaiclient.current_group_id();
Expand Down Expand Up @@ -724,9 +728,9 @@ export default class BackendAIData extends BackendAIPage {
this.openDialog('add-folder-dialog');
}

async _getStorageProxyBackendInformation() {
async _getVolumeInformation() {
const vhostInfo = await globalThis.backendaiclient.vfolder.list_hosts();
this.storageProxyInfo = vhostInfo.volume_info || {};
this.volumeInfo = vhostInfo.volume_info || {};
}

openDialog(id: string) {
Expand Down
99 changes: 72 additions & 27 deletions src/components/backend-ai-storage-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import {get as _text, translate as _t} from 'lit-translate';
import {css, CSSResultGroup, html, render} from 'lit';
import {customElement, property, query} from 'lit/decorators.js';
import {customElement, property, query, state} from 'lit/decorators.js';
import {BackendAIPage} from './backend-ai-page';

import './backend-ai-dialog';
Expand Down Expand Up @@ -141,7 +141,7 @@ export default class BackendAiStorageList extends BackendAIPage {
mem: 0.5
};
@property({type: Array}) filebrowserSupportedImages = [];
@property({type: Object}) storageProxyInfo = Object();
@property({type: Object}) volumeInfo = Object();
@property({type: Array}) quotaSupportStorageBackends = ['xfs', 'weka', 'spectrumscale'];
@property({type: Object}) quotaUnit = {
MiB: Math.pow(2, 20),
Expand All @@ -162,6 +162,7 @@ export default class BackendAiStorageList extends BackendAIPage {
@query('#modify-folder-quota') modifyFolderQuotaInput!: TextField;
@query('#modify-folder-quota-unit') modifyFolderQuotaUnitSelect!: Select;
@query('#fileList-grid') fileListGrid!: VaadinGrid;
@query('#folderList-grid') folderListGrid!: VaadinGrid;
@query('#mkdir-name') mkdirNameInput!: TextField;
@query('#delete-folder-name') deleteFolderNameInput!: TextField;
@query('#new-folder-name') newFolderNameInput!: TextField;
Expand All @@ -178,6 +179,7 @@ export default class BackendAiStorageList extends BackendAIPage {
@query('#modify-permission-dialog') modifyPermissionDialog!: BackendAIDialog;
@query('#share-folder-dialog') shareFolderDialog!: BackendAIDialog;
@query('#session-launcher') sessionLauncher!: BackendAiSessionLauncher;
@state() private _unionedAllowedPermissionByVolume = Object();
constructor() {
super();
this._boundIndexRenderer = this.indexRenderer.bind(this);
Expand Down Expand Up @@ -599,7 +601,7 @@ export default class BackendAiStorageList extends BackendAIPage {
id="session-launcher" ?active="${this.active === true}"
.newSessionDialogTitle="${_t('session.launcher.StartModelServing')}"></backend-ai-session-launcher>
<div class="list-wrapper">
<vaadin-grid class="folderlist" theme="row-stripes column-borders wrap-cell-content compact" column-reordering-allowed aria-label="Folder list" .items="${this.folders}">
<vaadin-grid class="folderlist" id="folderList-grid" theme="row-stripes column-borders wrap-cell-content compact" column-reordering-allowed aria-label="Folder list" .items="${this.folders}">
<vaadin-grid-column width="40px" flex-grow="0" resizable header="#" text-align="center" .renderer="${this._boundIndexRenderer}">
</vaadin-grid-column>
<lablup-grid-sort-filter-column path="name" width="80px" resizable .renderer="${this._boundFolderListRenderer}"
Expand Down Expand Up @@ -1053,7 +1055,6 @@ export default class BackendAiStorageList extends BackendAIPage {
firstUpdated() {
this._addEventListenerDropZone();
this._mkdir = this._mkdir.bind(this);

this.fileListGrid.addEventListener('selected-items-changed', () => {
this._toggleFileListCheckbox();
});
Expand All @@ -1071,16 +1072,6 @@ export default class BackendAiStorageList extends BackendAIPage {
document.addEventListener('backend-ai-group-changed', (e) => this._refreshFolderList(true, 'group-changed'));
document.addEventListener('backend-ai-ui-changed', (e) => this._refreshFolderUI(e));
this._refreshFolderUI({'detail': {'mini-ui': globalThis.mini_ui}});
if (typeof globalThis.backendaiclient === 'undefined' || globalThis.backendaiclient === null || globalThis.backendaiclient.ready === false) {
document.addEventListener('backend-ai-connected', () => {
this._getStorageProxyBackendInformation();
this._triggerFolderListChanged();
}, true);
} else { // already connected
this._getStorageProxyBackendInformation();
this._triggerFolderListChanged();
}

//@ts-ignore
const params = (new URL(document.location)).searchParams;
const folderName = params.get('folder');
Expand Down Expand Up @@ -1308,6 +1299,7 @@ export default class BackendAiStorageList extends BackendAIPage {
* @param {Object} rowData - the object with the properties related with the rendered item
* */
controlFolderListRenderer(root, column?, rowData?) {
const isSharingAllowed = (this._unionedAllowedPermissionByVolume[rowData.item.host]?? []).includes('invite-others');
render(
// language=HTML
html`
Expand Down Expand Up @@ -1351,13 +1343,15 @@ export default class BackendAiStorageList extends BackendAIPage {
title=${_t('data.explorer.ShareFolder')}
@click="${(e) => this._shareFolderDialog(e)}"
?disabled="${this._checkProcessingStatus(rowData.item.status)}"
style="display: ${isSharingAllowed ? '': 'none'}"
></mwc-icon-button>
<mwc-icon-button
class="fg cyan controls-running"
icon="perm_identity"
title=${_t('data.explorer.ModifyPermissions')}
@click=${(e) => (this._modifyPermissionDialog(rowData.item.id))}
?disabled="${this._checkProcessingStatus(rowData.item.status)}"
style="display: ${isSharingAllowed ? '': 'none'}"
></mwc-icon-button>
<mwc-icon-button
class="fg ${rowData.item.type == 'user' ? 'blue' : 'green'} controls-running"
Expand Down Expand Up @@ -1552,22 +1546,57 @@ export default class BackendAiStorageList extends BackendAIPage {
);
}

async _getStorageProxyBackendInformation() {
private async _getCurrentKeypairResourcePolicy() {
const accessKey = globalThis.backendaiclient._config.accessKey;
const res = await globalThis.backendaiclient.keypair.info(accessKey, ['resource_policy']);
return res.keypair.resource_policy;
}

async _getVolumeInformation() {
const vhostInfo = await globalThis.backendaiclient.vfolder.list_hosts();
this.storageProxyInfo = vhostInfo.volume_info || {};
this.volumeInfo = vhostInfo.volume_info || {};
}

async _getAllowedVFolderHostsByCurrentUserInfo() {
const [vhostInfo, currentKeypairResourcePolicy] = await Promise.all([
globalThis.backendaiclient.vfolder.list_hosts(),
this._getCurrentKeypairResourcePolicy(),
]);
const currentDomain = globalThis.backendaiclient._config.domainName;
const currentGroupId = globalThis.backendaiclient.current_group_id();
const mergedData = await globalThis.backendaiclient.storageproxy.getAllowedVFolderHostsByCurrentUserInfo(currentDomain, currentGroupId, currentKeypairResourcePolicy);

const allowedPermissionForDomainsByVolume = JSON.parse(mergedData.domain.allowed_vfolder_hosts);
const allowedPermissionForGroupsByVolume = JSON.parse(mergedData.group.allowed_vfolder_hosts);
const allowedPermissionForResourcePolicyByVolume = JSON.parse(mergedData.keypair_resource_policy.allowed_vfolder_hosts);

const _mergeDedupe = (arr) => [...new Set([].concat(...arr))];
this._unionedAllowedPermissionByVolume = Object.assign({}, ...vhostInfo.allowed.map((volume) => {
return {
[volume]: _mergeDedupe([
allowedPermissionForDomainsByVolume[volume],
allowedPermissionForGroupsByVolume[volume],
allowedPermissionForResourcePolicyByVolume[volume],
])
};
}));
this.folderListGrid.clearCache();
}

_checkFolderSupportSizeQuota(host: string) {
if (!host) {
return false;
}
const backend = this.storageProxyInfo[host]?.backend;
const backend = this.volumeInfo[host]?.backend;
return this.quotaSupportStorageBackends.includes(backend) ? true : false;
}

refreshFolderList() {
async refreshFolderList() {
this._triggerFolderListChanged();
return this._refreshFolderList(true, 'refreshFolderList');
if (this.folderListGrid) {
this.folderListGrid.clearCache();
}
return await this._refreshFolderList(true, 'refreshFolderList');
}

/**
Expand Down Expand Up @@ -1636,10 +1665,20 @@ export default class BackendAiStorageList extends BackendAIPage {
*
*/
async _checkFilebrowserSupported() {
const response = await globalThis.backendaiclient.image.list(['name', 'tag', 'registry', 'digest', 'installed', 'labels { key value }', 'resource_limits { key min max }'], false, true);
const fields = [
'name', 'tag', 'registry', 'digest', 'installed',
'labels { key value }',
'resource_limits { key min max }',
];
const response = await globalThis.backendaiclient.image.list(fields, true, true);
const images = response.images;
// only filter both installed and filebrowser supported image from images
this.filebrowserSupportedImages = images.filter((image) => image['installed'] && image.labels.find((label) => label.key === 'ai.backend.service-ports' && label.value.toLowerCase().includes('filebrowser')));
// Filter filebrowser supported images.
this.filebrowserSupportedImages = images.filter((image) =>
image.labels.find((label) =>
label.key === 'ai.backend.service-ports' &&
label.value.toLowerCase().includes('filebrowser'),
),
);
}

async _viewStateChanged(active) {
Expand All @@ -1648,14 +1687,17 @@ export default class BackendAiStorageList extends BackendAIPage {
return;
}
if (typeof globalThis.backendaiclient === 'undefined' || globalThis.backendaiclient === null || globalThis.backendaiclient.ready === false) {
document.addEventListener('backend-ai-connected', () => {
document.addEventListener('backend-ai-connected', async () => {
this.is_admin = globalThis.backendaiclient.is_admin;
this.enableStorageProxy = globalThis.backendaiclient.supports('storage-proxy');
this.enableInferenceWorkload = globalThis.backendaiclient.supports('inference-workload');
this.authenticated = true;
this._APIMajorVersion = globalThis.backendaiclient.APIMajorVersion;
this._maxFileUploadSize = globalThis.backendaiclient._config.maxFileUploadSize;
this._getAllowedVFolderHostsByCurrentUserInfo();
this._checkFilebrowserSupported();
this._getVolumeInformation();
this._triggerFolderListChanged();
this._refreshFolderList(false, 'viewStatechanged');
}, true);
} else {
Expand All @@ -1665,7 +1707,10 @@ export default class BackendAiStorageList extends BackendAIPage {
this.authenticated = true;
this._APIMajorVersion = globalThis.backendaiclient.APIMajorVersion;
this._maxFileUploadSize = globalThis.backendaiclient._config.maxFileUploadSize;
this._getAllowedVFolderHostsByCurrentUserInfo();
this._checkFilebrowserSupported();
this._getVolumeInformation();
this._triggerFolderListChanged();
this._refreshFolderList(false, 'viewStatechanged');
}
}
Expand Down Expand Up @@ -1912,15 +1957,15 @@ export default class BackendAiStorageList extends BackendAIPage {
* */
_deleteFolder(folderName) {
const job = globalThis.backendaiclient.vfolder.delete(folderName);
job.then((resp) => {
job.then(async (resp) => {
// console.log(resp);
if (resp.msg) {
this.notification.text = _text('data.folders.CannotDeleteFolder');
this.notification.show(true);
} else {
this.notification.text = _text('data.folders.FolderDeleted');
this.notification.show();
this.refreshFolderList();
await this.refreshFolderList();
this._triggerFolderListChanged();
}
}).catch((err) => {
Expand Down Expand Up @@ -2008,10 +2053,10 @@ export default class BackendAiStorageList extends BackendAIPage {
* */
_leaveFolder(folderId) {
const job = globalThis.backendaiclient.vfolder.leave_invited(folderId);
job.then((value) => {
job.then(async (value) => {
this.notification.text = _text('data.folders.FolderDisconnected');
this.notification.show();
this.refreshFolderList();
await this.refreshFolderList();
this._triggerFolderListChanged();
}).catch((err) => {
console.log(err);
Expand Down
21 changes: 21 additions & 0 deletions src/lib/backend.ai-client-esm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2315,6 +2315,27 @@ class StorageProxy {
return this.client._wrapWithPromise(rqst);
}
}

/**
* Get all fields related to allowed_vfolder_hosts according to the current user information
*
* @param {string} domainName
* @param {string} projectId
* @param {string} resourcePolicyName
* @returns {object} - get allowed_vfolder_hosts key-value on domain, group, resource policy of current user
*/
async getAllowedVFolderHostsByCurrentUserInfo(domainName = '', projectId = '', resourcePolicyName = '') {
const q = `
query($domainName: String, $projectId: UUID!, $resourcePolicyName: String) {
domain(name: $domainName) { allowed_vfolder_hosts }
group(id: $projectId, domain_name: $domainName) { allowed_vfolder_hosts }
keypair_resource_policy(name: $resourcePolicyName) { allowed_vfolder_hosts }
}
`;
const v = { domainName, projectId, resourcePolicyName };
return this.client.query(q, v);
}

}

class Keypair {
Expand Down

0 comments on commit d4b2023

Please sign in to comment.