static_src/stores/service_instance_store.js
/*
* Store for services data. Will store and update services data on changes from
* UI and server.
*/
import AppDispatcher from "../dispatcher";
import BaseStore from "./base_store.js";
import {
appStates,
serviceActionTypes,
errorActionTypes
} from "../constants.js";
import ServiceStore from "./service_store.js";
import ServicePlanStore from "./service_plan_store.js";
export const OPERATION_FAILED = "failed";
export const OPERATION_DELETING = "deleting";
const OPERATION_PROCESSING = "processing";
export const OPERATION_RUNNING = "running";
const OPERATION_INACTIVE = "inactive";
export const CREATED_NOTIFICATION_TIME_MS = 3000;
const OPERATION_STATES = {};
OPERATION_STATES[OPERATION_FAILED] = "Failed";
OPERATION_STATES[OPERATION_DELETING] = "Deleting";
OPERATION_STATES[OPERATION_PROCESSING] = "Reconfiguring";
OPERATION_STATES[OPERATION_RUNNING] = "Available";
OPERATION_STATES[OPERATION_INACTIVE] = "Stopped";
const APP_STATE_MAP = {
[OPERATION_FAILED]: appStates.crashed,
[OPERATION_DELETING]: appStates.stopped,
[OPERATION_INACTIVE]: appStates.stopped,
[OPERATION_PROCESSING]: appStates.running,
[OPERATION_RUNNING]: appStates.running
};
const SERVICE_INSTANCE_CREATE_ERROR_MAP = {
"CF-ServiceInstanceNameTaken":
"The service instance name is taken. Please use a unique name.",
"CF-ServiceInstanceInvalid": "Invalid space selected.",
"CF-ServiceBrokerBadResponse":
"This service instance must be created using the CF CLI." +
" Please refer to https://cloud.gov/docs/services/ for more information.",
"CF-MessageParseError": "One or more form fields are blank or invalid."
};
const BINDING_ERROR_MAP = {
"CF-ServiceBindingAppServiceTaken":
"Service instance already bound to the current app",
"CF-BindingCannot": "Cannot bind service instance."
};
const getFriendlyError = (error, errorMap) => {
const { code, error_code: errorCode } = error;
if (errorCode in errorMap) {
return errorMap[errorCode];
}
return `Error #${code}. Please contact cloud.gov support for help troubleshooting this issue.`;
};
export class ServiceInstanceStore extends BaseStore {
constructor() {
super();
this.subscribe(() => this.handleAction.bind(this));
this.createInstanceForm = null;
this.createError = null;
this.createLoading = false;
this.createdTempNotification = false;
this.isFetchingAll = false;
this.isFetching = false;
this.isUpdating = false;
}
get updating() {
return this.isUpdating;
}
get loading() {
return this.isFetchingAll || this.isFetching;
}
getAllBySpaceGuid(spaceGuid) {
return this.getAll().filter(
serviceInstance => serviceInstance.space_guid === spaceGuid
);
}
getInstanceState(serviceInstance) {
const lastOp = serviceInstance.last_operation;
if (!lastOp) return OPERATION_RUNNING;
if (lastOp.state === "failed") {
return OPERATION_FAILED;
}
if (lastOp.type === "delete") {
return OPERATION_DELETING;
}
if (lastOp.type === "update") {
if (lastOp.state === "in progress") return OPERATION_PROCESSING;
}
return OPERATION_RUNNING;
}
getMappedAppState(serviceInstance) {
const serviceState = this.getInstanceState(serviceInstance);
return APP_STATE_MAP[serviceState];
}
getInstanceReadableState(serviceInstance) {
if (!serviceInstance.last_operation) return OPERATION_STATES.running;
let state = this.getInstanceState(serviceInstance);
if (state === OPERATION_FAILED) {
state = `serviceInstance.last_operation.type ${OPERATION_STATES[state]}`;
}
return OPERATION_STATES[state];
}
getServiceBindingForApp(appGuid, serviceInstance) {
if (!serviceInstance.serviceBindings.length) return null;
return serviceInstance.serviceBindings.find(
serviceBinding => serviceBinding.app_guid === appGuid
);
}
isInstanceBound(serviceInstance, serviceBindings) {
if (!serviceInstance.serviceBindings.length) return false;
let isBound = false;
serviceInstance.serviceBindings.forEach(instanceBinding => {
isBound = serviceBindings.find(
serviceBinding => instanceBinding.guid === serviceBinding.guid
);
});
return isBound;
}
handleAction(action) {
switch (action.type) {
case serviceActionTypes.SERVICE_INSTANCES_FETCH: {
this.isFetchingAll = true;
this.emitChange();
break;
}
case serviceActionTypes.SERVICE_INSTANCE_FETCH: {
// TODO this isn't really correct, because if fetching multiple
// instances they will clobber state. When fetching individual
// entities, we should store the fetch state on the entity itself
this.isFetching = true;
this.emitChange();
break;
}
case serviceActionTypes.SERVICE_INSTANCE_RECEIVED: {
this.isFetching = false;
const instance = action.serviceInstance;
this.merge("guid", instance, () => {
// Always emitchange as fetch state was changed.
this.emitChange();
});
break;
}
case serviceActionTypes.SERVICE_INSTANCES_RECEIVED: {
this.isFetchingAll = false;
const services = action.serviceInstances;
this.mergeMany("guid", services, () => {
// Always emitchange as fetch state was changed.
this.emitChange();
});
break;
}
case serviceActionTypes.SERVICE_INSTANCE_CREATE_FORM: {
AppDispatcher.waitFor([ServiceStore.dispatchToken]);
this.createInstanceForm = {
error: null,
service: ServiceStore.get(action.serviceGuid),
servicePlan: ServicePlanStore.get(action.servicePlanGuid)
};
this.emitChange();
break;
}
case serviceActionTypes.SERVICE_INSTANCE_CREATE_FORM_CANCEL:
this.createInstanceForm = null;
this.emitChange();
break;
case serviceActionTypes.SERVICE_INSTANCE_CREATE: {
this.createLoading = true;
// TODO create a "creating" service instance in the UI to update later
this.emitChange();
break;
}
case serviceActionTypes.SERVICE_INSTANCE_CREATED: {
this.createError = null;
this.createLoading = false;
this.createdTempNotification = true;
this.emitChange();
setTimeout(() => {
this.createInstanceForm = null;
this.createdTempNotification = false;
this.emitChange();
}, CREATED_NOTIFICATION_TIME_MS);
break;
}
case serviceActionTypes.SERVICE_INSTANCE_CREATE_ERROR: {
this.createInstanceForm = Object.assign(
{},
this.createInstanceForm || {},
{
error: {
description: getFriendlyError(
action.error,
SERVICE_INSTANCE_CREATE_ERROR_MAP
)
}
}
);
this.createLoading = false;
this.emitChange();
break;
}
case serviceActionTypes.SERVICE_INSTANCE_DELETE_CONFIRM: {
const exists = this.get(action.serviceInstanceGuid);
if (exists) {
const toConfirm = {
guid: action.serviceInstanceGuid,
confirmDelete: true
};
this.merge("guid", toConfirm, changed => {
if (changed) this.emitChange();
});
}
break;
}
case serviceActionTypes.SERVICE_INSTANCE_DELETE_CANCEL: {
const exists = this.get(action.serviceInstanceGuid);
if (exists) {
const toConfirm = {
guid: action.serviceInstanceGuid,
confirmDelete: false
};
this.merge("guid", toConfirm, changed => {
if (changed) this.emitChange();
});
}
break;
}
case serviceActionTypes.SERVICE_INSTANCE_DELETE: {
this.isUpdating = true;
const serviceInstance = this.get(action.serviceInstanceGuid);
const toDelete = Object.assign({}, serviceInstance, { deleting: true });
this.merge("guid", toDelete);
break;
}
case serviceActionTypes.SERVICE_INSTANCE_DELETED: {
this.isUpdating = false;
this.delete(action.serviceInstanceGuid);
break;
}
case serviceActionTypes.SERVICE_BIND: {
const instance = this.get(action.serviceInstanceGuid);
if (instance) {
const newInstance = Object.assign({}, instance, {
loading: "Binding"
});
this.merge("guid", newInstance, () => this.emitChange());
}
break;
}
case serviceActionTypes.SERVICE_UNBIND: {
const instance = this.get(action.serviceBinding.service_instance_guid);
if (instance) {
const newInstance = Object.assign({}, instance, {
loading: "Unbinding"
});
this.merge("guid", newInstance, () => this.emitChange());
}
break;
}
case serviceActionTypes.SERVICE_BOUND:
case serviceActionTypes.SERVICE_UNBOUND: {
let binding;
if (action.type === serviceActionTypes.SERVICE_BOUND) {
binding = action.serviceBinding;
} else {
binding = action.serviceBinding;
}
const instance = this.get(binding.service_instance_guid);
if (!instance) break; // TODO throw error
const updatedInstance = Object.assign({}, instance, {
changing: false,
error: false,
loading: false
});
this.merge("guid", updatedInstance, () => this.emitChange());
break;
}
case serviceActionTypes.SERVICE_INSTANCE_CHANGE_CHECK: {
const instance = this.get(action.serviceInstanceGuid);
if (!instance) break; // TODO throw error?
const updatedInstance = Object.assign({}, instance, {
changing: true
});
this.merge("guid", updatedInstance, () => this.emitChange());
break;
}
case serviceActionTypes.SERVICE_INSTANCE_CHANGE_CANCEL: {
const instance = this.get(action.serviceInstanceGuid);
if (!instance) break; // TODO throw error?
const updatedInstance = Object.assign({}, instance, {
changing: false
});
this.merge("guid", updatedInstance, () => this.emitChange());
break;
}
case serviceActionTypes.SERVICE_INSTANCE_ERROR: {
const instance = this.get(action.serviceInstanceGuid);
if (!instance) {
break;
}
const newInstance = Object.assign({}, instance, {
error: {
description: getFriendlyError(action.error, BINDING_ERROR_MAP)
},
loading: false
});
this.merge("guid", newInstance, changed => {
if (changed) this.emitChange();
});
break;
}
case errorActionTypes.CLEAR: {
const clearedInstances = this.storeData
.filter(val => val.has("error"))
.map(instance => instance.update("error", () => null));
this.createError = null;
this.mergeMany("guid", clearedInstances, () => {
this.emitChange();
});
break;
}
default:
break;
}
}
}
const serviceInstanceStore = new ServiceInstanceStore();
serviceInstanceStore.OPERATION_STATES = OPERATION_STATES;
export default serviceInstanceStore;
export { SERVICE_INSTANCE_CREATE_ERROR_MAP, BINDING_ERROR_MAP };