Skip to content

Commit

Permalink
Display errors manage filers (#216)
Browse files Browse the repository at this point in the history
* fixed bug with requesting shares creation

* updated ui to display errors

* fixed the type script

* removed print statement

* improved UI

* updated existing shares delete

* updated submit code

* fix build
  • Loading branch information
mathis-marcotte authored Oct 15, 2024
1 parent 8f9e427 commit 3000d60
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 162 deletions.
64 changes: 33 additions & 31 deletions components/centraldashboard/app/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ export const ERRORS = {
invalid_create_requesting_shares: 'Failed to create requesting shares configmap',
invalid_update_requesting_shares: 'Failed to update requesting shares configmap',
invalid_update_existing_shares: 'Failed to update existing shares configmap',
invalid_delete_existing_shares: 'Failed to delete existing shares configmap'
invalid_delete_existing_shares: 'Failed to delete existing shares configmap' ,
invalid_get_shares_errors: 'Failed to load shares errors',
invalid_delete_shares_errors: 'Failed to delete from shares errors',
};

export function apiError(a: {res: Response, error: string, code?: number}) {
Expand Down Expand Up @@ -124,19 +126,6 @@ export class Api {
}
res.json(filers);
})
.post(
'/create-requesting-shares/:namespace',
async (req: Request, res: Response) => {
try {
const cm = await this.k8sService.createRequestingSharesConfigMap(req.params.namespace, req.body, req.user.email);
res.json(cm.data);
}catch(e){
return apiError({
res, code: 500,
error: ERRORS.invalid_create_requesting_shares,
});
}
})
.get(
'/get-existing-shares/:namespace',
async (req: Request, res: Response) => {
Expand All @@ -163,7 +152,7 @@ export class Api {
});
}
})
.patch(
.post(
'/update-requesting-shares/:namespace',
async (req: Request, res: Response) => {
try {
Expand All @@ -176,32 +165,45 @@ export class Api {
});
}
})
.patch(
'/update-existing-shares/:namespace',
async (req: Request, res: Response) => {
try {
const cm = await this.k8sService.updateExistingSharesConfigMap(req.params.namespace, req.body);
res.json(cm.data);
}catch(e){
return apiError({
res, code: 500,
error: ERRORS.invalid_update_existing_shares,
});
}
})
.delete(
'/delete-existing-shares/:namespace',
'/delete-existing-share/:namespace',
async (req: Request, res: Response) => {
try {
await this.k8sService.deleteExistingSharesConfigMap(req.params.namespace);
await this.k8sService.deleteFromExistingSharesConfigMap(req.params.namespace, req.body);
res.json({});
}catch(e){
return apiError({
res, code: 500,
error: ERRORS.invalid_delete_existing_shares,
});
}
});
})
.get(
'/get-shares-errors/:namespace',
async (req: Request, res: Response) => {
try {
const cm = await this.k8sService.getSharesErrorsConfigMap(req.params.namespace);
res.json(cm.data);
}catch(e){
return apiError({
res, code: 500,
error: ERRORS.invalid_get_shares_errors,
});
}
})
.delete(
'/delete-shares-error/:namespace',
async (req: Request, res: Response) => {
try {
await this.k8sService.deleteFromSharesErrorsConfigMap(req.params.namespace, req.body);
res.json({});
}catch(e){
return apiError({
res, code: 500,
error: ERRORS.invalid_delete_shares_errors,
});
}
});
}

resolveLanguage(requested: string[], supported: string[], defaultLang: string) {
Expand Down
160 changes: 106 additions & 54 deletions components/centraldashboard/app/k8s_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const APP_API_VERSION = 'v1beta1';
const APP_API_NAME = 'applications';
const REQUESTING_SHARES_CM_NAME = 'requesting-shares';
const EXISTING_SHARES_CM_NAME = 'existing-shares';
const SHARES_ERRORS_CM_NAME = 'shares-errors';

/** Wrap Kubernetes API calls in a simpler interface for use in routes. */
export class KubernetesService {
Expand Down Expand Up @@ -118,106 +119,157 @@ export class KubernetesService {
}
}

/** Creates the requesting shares configmap for the central dashboard. */
async createRequestingSharesConfigMap(namespace: string, data: {[key:string]:string}, email: string): Promise<k8s.V1ConfigMap> {
/** Retrieves the existing shares configmap data for the central dashboard. */
async getExistingSharesConfigMap(namespace: string): Promise<k8s.V1ConfigMap> {
try {
const config = {
metadata: {
name: REQUESTING_SHARES_CM_NAME,
labels: {
"for-ontap": "true"
},
annotations: {
"user-email": email
}
},
data
} as k8s.V1ConfigMap;
const { body } = await this.coreAPI.createNamespacedConfigMap(namespace, config);
const { body } = await this.coreAPI.readNamespacedConfigMap(EXISTING_SHARES_CM_NAME, namespace);
return body;
} catch (err) {
console.error('Unable to create requesting shares ConfigMap:', err.response?.body || err.body || err);
if(err.statusCode === 404){
//user has no existing-shares yet
return new k8s.V1ConfigMap();
}
console.error('Unable to fetch ConfigMap:', err.response?.body || err.body || err);
throw err;
}
}

/** Retrieves the existing shares configmap data for the central dashboard. */
async getExistingSharesConfigMap(namespace: string): Promise<k8s.V1ConfigMap> {
/** Retrieves the requesting shares configmap data for the central dashboard. */
async getRequestingSharesConfigMap(namespace: string): Promise<k8s.V1ConfigMap> {
try {
const { body } = await this.coreAPI.readNamespacedConfigMap(EXISTING_SHARES_CM_NAME, namespace);
const { body } = await this.coreAPI.readNamespacedConfigMap(REQUESTING_SHARES_CM_NAME, namespace);
return body;
} catch (err) {
if(err.statusCode === 404){
//user has no user-filers yet
//user has no requesting-shares yet
return new k8s.V1ConfigMap();
}
console.error('Unable to fetch ConfigMap:', err.response?.body || err.body || err);
throw err;
}
}

/** Retrieves the requesting shares configmap data for the central dashboard. */
async getRequestingSharesConfigMap(namespace: string): Promise<k8s.V1ConfigMap> {
/** Retrieves the shares errors configmap data for the central dashboard. */
async getSharesErrorsConfigMap(namespace: string): Promise<k8s.V1ConfigMap> {
try {
const { body } = await this.coreAPI.readNamespacedConfigMap(REQUESTING_SHARES_CM_NAME, namespace);
const { body } = await this.coreAPI.readNamespacedConfigMap(SHARES_ERRORS_CM_NAME, namespace);
return body;
} catch (err) {
if(err.statusCode === 404){
//user has no user-filers yet
//user has no shares-errors yet
return new k8s.V1ConfigMap();
}
console.error('Unable to fetch ConfigMap:', err.response?.body || err.body || err);
throw err;
}
}

/** Updates the requesting shares configmap for the central dashboard. */
async updateRequestingSharesConfigMap(namespace: string, data: {[key:string]:string}, email: string): Promise<k8s.V1ConfigMap> {
/** Updates the requesting shares configmap for the central dashboard; Creates the configmap if it is not created. */
async updateRequestingSharesConfigMap(namespace: string, data: {svm: string, share: string}, email: string): Promise<k8s.V1ConfigMap> {
try {
const config = {
metadata: {
name: REQUESTING_SHARES_CM_NAME,
labels: {
"for-ontap": "true"
},
annotations: {
"user-email": email
}
},
data
} as k8s.V1ConfigMap;
const { body } = await this.coreAPI.replaceNamespacedConfigMap(REQUESTING_SHARES_CM_NAME, namespace, config);
//try to get the configmap to see if it exists
const getPromise = await this.coreAPI.readNamespacedConfigMap(REQUESTING_SHARES_CM_NAME, namespace);
const requestingCM = getPromise.body;

const svmSharesData: string[] = JSON.parse(requestingCM.data[data.svm]);
svmSharesData.push(data.share);

requestingCM.data[data.svm] = JSON.stringify(svmSharesData);

const { body } = await this.coreAPI.replaceNamespacedConfigMap(REQUESTING_SHARES_CM_NAME, namespace, requestingCM);
return body;
} catch (err) {
console.error('Unable to patch ConfigMap:', err.response?.body || err.body || err);
if(err.statusCode === 404){
//user has no requesting-shares yet, so we create it
const dataValue:{[key:string]:string} = {};
dataValue[data.svm]=JSON.stringify([data.share]);

const config = {
metadata: {
name: REQUESTING_SHARES_CM_NAME,
labels: {
"for-ontap": "true"
},
annotations: {
"user-email": email
}
},
data: dataValue
} as k8s.V1ConfigMap;

const { body } = await this.coreAPI.createNamespacedConfigMap(namespace, config);
return body;
}
console.error('Unable to update ConfigMap:', err.response?.body || err.body || err);
throw err;
}
}

/** Updates the existing shares configmap for the central dashboard. */
async updateExistingSharesConfigMap(namespace: string, data: {[key:string]:string}): Promise<k8s.V1ConfigMap> {
/** Deletes from the existing shares configmap for the central dashboard. */
async deleteFromExistingSharesConfigMap(namespace: string, data: {svm: string, share: string}): Promise<k8s.V1Status> {
try {
const config = {
metadata: {
name: EXISTING_SHARES_CM_NAME
},
data
} as k8s.V1ConfigMap;
const { body } = await this.coreAPI.replaceNamespacedConfigMap(EXISTING_SHARES_CM_NAME, namespace, config);
const getPromise = await this.coreAPI.readNamespacedConfigMap(EXISTING_SHARES_CM_NAME, namespace);
const existingCM = getPromise.body;

const svmSharesData: string[] = JSON.parse(existingCM.data[data.svm]);

const deleteIndex = svmSharesData.indexOf(data.share);
svmSharesData.splice(deleteIndex, 1);

//if CM would be empty, just delete it
if (svmSharesData.length === 0 && Object.keys(existingCM.data).length===1){
const deletePromise = await this.coreAPI.deleteNamespacedConfigMap(EXISTING_SHARES_CM_NAME, namespace);
return deletePromise.body;
}

existingCM.data[data.svm] = JSON.stringify(svmSharesData);

const { body } = await this.coreAPI.replaceNamespacedConfigMap(EXISTING_SHARES_CM_NAME, namespace, existingCM);
return body;
} catch (err) {
console.error('Unable to patch ConfigMap:', err.response?.body || err.body || err);
console.error('Unable to delete from ConfigMap:', err.response?.body || err.body || err);
throw err;
}
}

/** Deletes the existing shares configmap for the central dashboard. */
async deleteExistingSharesConfigMap(namespace: string): Promise<k8s.V1Status> {
/** Removes a value from the shares-errors configmap; Deletes the configmap if it would be empty */
async deleteFromSharesErrorsConfigMap(namespace: string, data: {ErrorMessage: string, Svm: string, Share: string, Timestamp: string}): Promise<k8s.V1ConfigMap> {
try {
const { body } = await this.coreAPI.deleteNamespacedConfigMap(EXISTING_SHARES_CM_NAME, namespace);
const getPromise = await this.coreAPI.readNamespacedConfigMap(SHARES_ERRORS_CM_NAME, namespace);

const errorsData:Array<{ErrorMessage: string, Svm: string, Share: string, Timestamp: string}> = JSON.parse(getPromise.body.data.errors);

//find the index of the element to delete
const deleteIndex = errorsData.findIndex(d =>
d.ErrorMessage === data.ErrorMessage &&
d.Share === data.Share &&
d.Svm === data.Svm &&
d.Timestamp === data.Timestamp
);

//delete element
errorsData.splice(deleteIndex, 1);

//delete the CM if the value would be empty
if(errorsData.length===0){
const deletePromise = await this.coreAPI.deleteNamespacedConfigMap(SHARES_ERRORS_CM_NAME, namespace);
return deletePromise.body;
}

const config = {
metadata: {
name: SHARES_ERRORS_CM_NAME
},
data: {
errors: JSON.stringify(errorsData)
}
} as k8s.V1ConfigMap;

//Update the configmap
const { body } = await this.coreAPI.replaceNamespacedConfigMap(SHARES_ERRORS_CM_NAME, namespace, config);
return body;
} catch (err) {
console.error('Unable to delete ConfigMap:', err.response?.body || err.body || err);
console.error('Unable to delete from ConfigMap:', err.response?.body || err.body || err);
throw err;
}
}
Expand Down
14 changes: 8 additions & 6 deletions components/centraldashboard/public/assets/i18n/languages.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,12 @@
"manageFilersView.lblShare": "Share path",
"manageFilersView.placeholderShare": "Please input a valid share path (ex /data/PROD)",
"manageFilersView.btnSubmit": "Submit",
"manageFilersView.successUpdate": "Filers updated!",
"manageFilersView.successUpdate": "Filer shares updated!",
"manageFilersView.missingFiler": "Please select a filer",
"manageFilersView.invalidSharePath": "Please input a valid share path",
"manageFilersView.duplicateFiler": "Filer has already been selected",
"manageFilersView.missingDeleteError": "User does not have filer {filerShare}",
"manageFilersView.duplicateFiler": "Filer share has already been selected",
"manageFilersView.missingDeleteError": "User does not have filer share {filerShare}",
"manageFilersView.removeErrorError": "Failed to remove error",
"s3Proxy.defaultMessage": "This workspace does not include S3 proxy. To start using S3 proxy, please opt in to the service."
},
"fr": {
Expand Down Expand Up @@ -288,11 +289,12 @@
"manageFilersView.lblShare": "Chemin de partage",
"manageFilersView.placeholderShare": "Veuillez saisir un chemin de partage valide (ex /data/PROD)",
"manageFilersView.btnSubmit": "Soumettre",
"manageFilersView.successUpdate": "Classeurs mis à jour!",
"manageFilersView.successUpdate": "Chemins de partage mis à jour!",
"manageFilersView.missingFiler": "Veuillez sélectionner un classeur",
"manageFilersView.invalidSharePath": "Veuillez saisir un chemin de partage valide",
"manageFilersView.duplicateFiler": "Le classeur a déjà été sélectionné",
"manageFilersView.missingDeleteError": "L'utilisateur n'a pas le classeur {filerShare}",
"manageFilersView.duplicateFiler": "Le chemin de partage a déjà été sélectionné",
"manageFilersView.missingDeleteError": "L'utilisateur n'a pas le chemin de partage {filerShare}",
"manageFilersView.removeErrorError": "Échec de la suppression de l'erreur",
"s3Proxy.defaultMessage": "Cet espace de travail n'inclue pas S3 proxy. Pour commencer à utiliser S3 proxy, veuilliez choisir de participer au service."
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ h2 .icon {
.filersTable{
margin-bottom: 1.5em;
border-collapse: collapse;
border: 1px solid gray;
}

table, th, td {
.filersTable th, .filersTable td {
border: 1px solid gray;
}

Expand Down Expand Up @@ -101,3 +102,8 @@ label {
.formInputDiv span select{
width: 97%;
}

.errorDiv{
background-color: #ff9b9b;
color: #212324;
}
Loading

0 comments on commit 3000d60

Please sign in to comment.