Revision 198b546d snf-cyclades-app/synnefo/ui/static/snf/js/models.js
b/snf-cyclades-app/synnefo/ui/static/snf/js/models.js | ||
---|---|---|
1 |
// Copyright 2011 GRNET S.A. All rights reserved.
|
|
1 |
// Copyright 2014 GRNET S.A. All rights reserved.
|
|
2 | 2 |
// |
3 | 3 |
// Redistribution and use in source and binary forms, with or |
4 | 4 |
// without modification, are permitted provided that the following |
... | ... | |
334 | 334 |
api: snf.api, |
335 | 335 |
api_type: 'compute', |
336 | 336 |
supportIncUpdates: true, |
337 |
|
|
338 |
mapAttrs: function() { |
|
339 |
var params = _.toArray(arguments); |
|
340 |
return this.map(function(i) { |
|
341 |
return _.map(params, function(attr) { |
|
342 |
return i.get(attr); |
|
343 |
}); |
|
344 |
}) |
|
345 |
}, |
|
346 |
|
|
347 |
mapAttr: function(attr) { |
|
348 |
return this.map(function(i) { return i.get(attr)}) |
|
349 |
}, |
|
350 |
|
|
351 |
filterAttr: function(attr, eq) { |
|
352 |
return this.filter(function(i) { return i.get(attr) === eq}) |
|
353 |
}, |
|
337 | 354 |
|
338 | 355 |
initialize: function() { |
339 | 356 |
models.Collection.__super__.initialize.apply(this, arguments); |
... | ... | |
353 | 370 |
return getUrl.call(this, this.base_url) + ( |
354 | 371 |
options.details || this.details && method != 'create' ? '/detail' : ''); |
355 | 372 |
}, |
373 |
|
|
374 |
delay_fetch: function(delay, options) { |
|
375 |
window.setTimeout(_.bind(function() { |
|
376 |
this.fetch(options) |
|
377 |
}, this), delay); |
|
378 |
}, |
|
356 | 379 |
|
357 | 380 |
fetch: function(options) { |
358 | 381 |
if (!options) { options = {} }; |
... | ... | |
392 | 415 |
if (coll.add_on_create) { |
393 | 416 |
coll.add(nextModel, options); |
394 | 417 |
} |
418 |
synnefo.api.trigger("quota:update"); |
|
395 | 419 |
if (success) success(nextModel, resp, xhr); |
396 | 420 |
}; |
397 | 421 |
model.save(null, options); |
... | ... | |
618 | 642 |
return parseInt(this.get("ram")) * 1024 * 1024; |
619 | 643 |
}, |
620 | 644 |
|
645 |
get_readable: function(key) { |
|
646 |
var parser = function(v) { return v } |
|
647 |
var getter = this.get; |
|
648 |
if (key == 'ram') { |
|
649 |
parser = synnefo.util.readablizeBytes |
|
650 |
getter = this.ram_to_bytes |
|
651 |
} |
|
652 |
if (key == 'disk') { |
|
653 |
parser = synnefo.util.readablizeBytes |
|
654 |
getter = this.ram_to_bytes |
|
655 |
} |
|
656 |
if (key == 'cpu') { |
|
657 |
parser = function(v) { return v + 'x' } |
|
658 |
} |
|
659 |
|
|
660 |
var val = getter.call(this, key); |
|
661 |
return parser(val); |
|
662 |
}, |
|
663 |
|
|
664 |
quotas: function() { |
|
665 |
return { |
|
666 |
'cyclades.vm': 1, |
|
667 |
'cyclades.disk': this.disk_to_bytes(), |
|
668 |
'cyclades.ram': this.ram_to_bytes(), |
|
669 |
'cyclades.cpu': this.get('cpu') |
|
670 |
} |
|
671 |
} |
|
672 |
|
|
621 | 673 |
}); |
622 | 674 |
|
623 | 675 |
models.ParamsList = function(){this.initialize.apply(this, arguments)}; |
... | ... | |
737 | 789 |
] |
738 | 790 |
}, |
739 | 791 |
|
792 |
storage_attrs: { |
|
793 |
'tenant_id': ['projects', 'project'] |
|
794 |
}, |
|
795 |
|
|
740 | 796 |
initialize: function(params) { |
741 | 797 |
var self = this; |
742 | 798 |
this.ports = new Backbone.FilteredCollection(undefined, { |
... | ... | |
1104 | 1160 |
}, |
1105 | 1161 |
|
1106 | 1162 |
can_start: function(flv, count_current) { |
1163 |
var self = this; |
|
1107 | 1164 |
var get_quota = function(key) { |
1108 |
return synnefo.storage.quotas.get(key).get('available'); |
|
1165 |
if (!self.get('project')) { return false } |
|
1166 |
return self.get('project').quotas.get(key).get('available'); |
|
1109 | 1167 |
} |
1110 | 1168 |
var flavor = flv || this.get_flavor(); |
1111 | 1169 |
var vm_ram_current = 0, vm_cpu_current = 0; |
... | ... | |
1136 | 1194 |
return this.get('status') == 'STOPPED'; |
1137 | 1195 |
}, |
1138 | 1196 |
|
1197 |
can_reassign: function() { |
|
1198 |
return true; |
|
1199 |
}, |
|
1200 |
|
|
1139 | 1201 |
handle_stats_error: function() { |
1140 | 1202 |
stats = {}; |
1141 | 1203 |
_.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) { |
... | ... | |
1399 | 1461 |
// call rename api |
1400 | 1462 |
rename: function(new_name) { |
1401 | 1463 |
//this.set({'name': new_name}); |
1464 |
var self = this; |
|
1402 | 1465 |
this.sync("update", this, { |
1403 | 1466 |
critical: true, |
1404 | 1467 |
data: { |
... | ... | |
1408 | 1471 |
}, |
1409 | 1472 |
success: _.bind(function(){ |
1410 | 1473 |
snf.api.trigger("call"); |
1474 |
this.set({'name': new_name}); |
|
1411 | 1475 |
}, this) |
1412 | 1476 |
}); |
1413 | 1477 |
}, |
... | ... | |
1515 | 1579 |
// set state after successful call |
1516 | 1580 |
self.state('DESTROY'); |
1517 | 1581 |
success.apply(this, arguments); |
1518 |
|
|
1582 |
synnefo.api.trigger("quotas:call", 20); |
|
1519 | 1583 |
}, |
1520 | 1584 |
error, 'destroy', params); |
1521 | 1585 |
break; |
1586 |
case 'reassign': |
|
1587 |
this.__make_api_call(this.get_action_url(), // vm actions url |
|
1588 |
"create", // create so that sync later uses POST to make the call |
|
1589 |
{reassign: {project:params.project_id}}, // payload |
|
1590 |
function() { |
|
1591 |
self.state('reassign'); |
|
1592 |
self.set({'tenant_id': params.project_id}); |
|
1593 |
success.apply(this, arguments); |
|
1594 |
snf.api.trigger("call"); |
|
1595 |
}, |
|
1596 |
error, 'reassign', params); |
|
1597 |
break; |
|
1522 | 1598 |
case 'resize': |
1523 | 1599 |
this.__make_api_call(this.get_action_url(), // vm actions url |
1524 | 1600 |
"create", // create so that sync later uses POST to make the call |
... | ... | |
1660 | 1736 |
'console', |
1661 | 1737 |
'destroy', |
1662 | 1738 |
'resize', |
1739 |
'reassign', |
|
1663 | 1740 |
'snapshot' |
1664 | 1741 |
] |
1665 | 1742 |
|
... | ... | |
1669 | 1746 |
'STOPPING': 'SHUTDOWN', |
1670 | 1747 |
'STARTING': 'START', |
1671 | 1748 |
'RESIZING': 'RESIZE', |
1749 |
'REASSIGNING': 'REASSIGN', |
|
1672 | 1750 |
'CONNECTING': 'CONNECT', |
1673 | 1751 |
'DISCONNECTING': 'DISCONNECT', |
1674 | 1752 |
'DESTROYING': 'DESTROY' |
... | ... | |
1678 | 1756 |
'UNKNWON' : ['destroy'], |
1679 | 1757 |
'BUILD' : ['destroy'], |
1680 | 1758 |
'REBOOT' : ['destroy'], |
1681 |
'STOPPED' : ['start', 'destroy', 'resize', 'snapshot'], |
|
1682 |
'ACTIVE' : ['shutdown', 'destroy', 'reboot', 'console', 'resize', 'snapshot'], |
|
1759 |
'STOPPED' : ['start', 'destroy', 'reassign', 'resize', 'snapshot'],
|
|
1760 |
'ACTIVE' : ['shutdown', 'destroy', 'reboot', 'console', 'reassign', 'resize', 'snapshot'],
|
|
1683 | 1761 |
'ERROR' : ['destroy'], |
1684 | 1762 |
'DELETED' : ['destroy'], |
1685 | 1763 |
'DESTROY' : ['destroy'], |
... | ... | |
1687 | 1765 |
'START' : ['destroy'], |
1688 | 1766 |
'CONNECT' : ['destroy'], |
1689 | 1767 |
'DISCONNECT' : ['destroy'], |
1690 |
'RESIZE' : ['destroy'] |
|
1768 |
'RESIZE' : ['destroy'], |
|
1769 |
'REASSIGN' : ['destroy'] |
|
1691 | 1770 |
} |
1692 | 1771 |
|
1693 | 1772 |
models.VM.AVAILABLE_ACTIONS_INACTIVE = {} |
... | ... | |
1701 | 1780 |
'ACTIVE', |
1702 | 1781 |
'ERROR', |
1703 | 1782 |
'DELETED', |
1783 |
'REASSIGN', |
|
1704 | 1784 |
'RESIZE' |
1705 | 1785 |
] |
1706 | 1786 |
|
... | ... | |
1719 | 1799 |
'CONNECT', |
1720 | 1800 |
'DISCONNECT', |
1721 | 1801 |
'FIREWALL', |
1802 |
'REASSIGN', |
|
1722 | 1803 |
'RESIZE' |
1723 | 1804 |
]); |
1724 | 1805 |
|
1725 | 1806 |
models.VM.STATES_TRANSITIONS = { |
1726 | 1807 |
'DESTROY' : ['DELETED'], |
1727 | 1808 |
'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'], |
1728 |
'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY'], |
|
1729 |
'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY'], |
|
1809 |
'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY', 'RESIZE', 'REASSIGN'],
|
|
1810 |
'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY', 'REASSIGN'],
|
|
1730 | 1811 |
'START': ['ERROR', 'ACTIVE', 'DESTROY'], |
1731 | 1812 |
'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'], |
1732 | 1813 |
'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'], |
1733 |
'RESIZE': ['ERROR', 'STOPPED'] |
|
1814 |
'RESIZE': ['ERROR', 'STOPPED'], |
|
1815 |
'REASSIGN': ['ERROR', 'STOPPED', 'ACTIVE'] |
|
1734 | 1816 |
} |
1735 | 1817 |
|
1736 | 1818 |
models.VM.TRANSITION_STATES = [ |
... | ... | |
1740 | 1822 |
'REBOOT', |
1741 | 1823 |
'BUILD', |
1742 | 1824 |
'RESIZE', |
1825 |
'REASSIGN', |
|
1743 | 1826 |
'DISCONNECT', |
1744 | 1827 |
'CONNECT' |
1745 | 1828 |
] |
... | ... | |
2165 | 2248 |
return vm_data.metadata && vm_data.metadata |
2166 | 2249 |
}, |
2167 | 2250 |
|
2168 |
create: function (name, image, flavor, meta, extra, callback) { |
|
2251 |
create: function (name, image, flavor, meta, project, extra, callback) {
|
|
2169 | 2252 |
|
2170 | 2253 |
if (this.copy_image_meta) { |
2171 | 2254 |
if (synnefo.config.vm_image_common_metadata) { |
... | ... | |
2183 | 2266 |
} |
2184 | 2267 |
|
2185 | 2268 |
opts = {name: name, imageRef: image.id, flavorRef: flavor.id, |
2186 |
metadata:meta} |
|
2269 |
metadata:meta, project: project.id}
|
|
2187 | 2270 |
opts = _.extend(opts, extra); |
2188 | 2271 |
|
2189 | 2272 |
var cb = function(data) { |
2190 |
synnefo.storage.quotas.get('cyclades.vm').increase();
|
|
2273 |
synnefo.api.trigger("quota:update");
|
|
2191 | 2274 |
callback(data); |
2192 | 2275 |
} |
2193 | 2276 |
|
... | ... | |
2260 | 2343 |
|
2261 | 2344 |
rename: function(new_name) { |
2262 | 2345 |
//this.set({'name': new_name}); |
2346 |
var self = this; |
|
2263 | 2347 |
this.sync("update", this, { |
2264 | 2348 |
critical: true, |
2265 | 2349 |
data: {'name': new_name}, |
2266 | 2350 |
success: _.bind(function(){ |
2267 | 2351 |
snf.api.trigger("call"); |
2352 |
this.set({'name': new_name}); |
|
2268 | 2353 |
}, this) |
2269 | 2354 |
}); |
2270 | 2355 |
}, |
... | ... | |
2400 | 2485 |
|
2401 | 2486 |
|
2402 | 2487 |
models.Quota = models.Model.extend({ |
2488 |
storage_attrs: { |
|
2489 |
'project_id': ['projects', 'project'] |
|
2490 |
}, |
|
2403 | 2491 |
|
2404 | 2492 |
initialize: function() { |
2405 | 2493 |
models.Quota.__super__.initialize.apply(this, arguments); |
... | ... | |
2476 | 2564 |
return snf.util.readablizeBytes(value); |
2477 | 2565 |
} |
2478 | 2566 |
}); |
2479 |
|
|
2567 |
|
|
2480 | 2568 |
models.Quotas = models.Collection.extend({ |
2481 | 2569 |
model: models.Quota, |
2482 | 2570 |
api_type: 'accounts', |
2483 | 2571 |
path: 'quotas', |
2484 | 2572 |
supportIncUpdates: false, |
2573 |
|
|
2574 |
required_quota: { |
|
2575 |
'vm': { |
|
2576 |
'cyclades.vm': 1, |
|
2577 |
'cyclades.ram': 1, |
|
2578 |
'cyclades.cpu': 1, |
|
2579 |
'cyclades.disk': 1 |
|
2580 |
}, |
|
2581 |
'network': { |
|
2582 |
'cyclades.network.private': 1 |
|
2583 |
}, |
|
2584 |
'ip': { |
|
2585 |
'cyclades.floating_ip': 1 |
|
2586 |
} |
|
2587 |
}, |
|
2588 |
|
|
2485 | 2589 |
parse: function(resp) { |
2486 |
filtered = _.map(resp.system, function(value, key) { |
|
2487 |
var available = (value.limit - value.usage) || 0; |
|
2590 |
var parsed = []; |
|
2591 |
_.each(resp, function(resources, uuid) { |
|
2592 |
parsed = _.union(parsed, _.map(resources, function(value, key) { |
|
2593 |
var quota_available = value.limit - value.usage || 0; |
|
2594 |
var total_available = quota_available; |
|
2595 |
var project_available = value.project_limit - value.project_usage || 0; |
|
2596 |
var limit = value.limit; |
|
2597 |
var usage = value.usage; |
|
2598 |
|
|
2599 |
var available = quota_available; |
|
2600 |
var total_available = available; |
|
2601 |
|
|
2602 |
// priority to project limits |
|
2603 |
if (project_available < available ) { |
|
2604 |
available = project_available; |
|
2605 |
limit = value.project_limit; |
|
2606 |
usage = value.project_usage; |
|
2607 |
total_available = available; |
|
2608 |
} |
|
2609 |
|
|
2488 | 2610 |
var available_active = available; |
2611 |
|
|
2612 |
// corresponding total quota |
|
2489 | 2613 |
var keysplit = key.split("."); |
2490 |
var limit_active = value.limit; |
|
2491 |
var usage_active = value.usage; |
|
2492 |
keysplit[keysplit.length-1] = "total_" + keysplit[keysplit.length-1]; |
|
2493 |
var activekey = keysplit.join("."); |
|
2494 |
var exists = resp.system[activekey]; |
|
2495 |
if (exists) { |
|
2496 |
available_active = exists.limit - exists.usage; |
|
2497 |
limit_active = exists.limit; |
|
2498 |
usage_active = exists.usage; |
|
2614 |
var last_part = keysplit.pop(); |
|
2615 |
var activekey = keysplit.join(".") + "." + "total_" + last_part; |
|
2616 |
var total = resp[uuid][activekey]; |
|
2617 |
if (total) { |
|
2618 |
total_available = total.limit - total.usage; |
|
2619 |
var total_project_available = total.project_limit - total.project_usage; |
|
2620 |
var total_limit = total.limit; |
|
2621 |
var total_usage = total.usage; |
|
2622 |
if (total_project_available < total_available) { |
|
2623 |
total_available = total_project_available; |
|
2624 |
total_limit = total.project_limit; |
|
2625 |
total_usage = total.project_usage; |
|
2626 |
} |
|
2627 |
if (total_available < available) { |
|
2628 |
available = total_available; |
|
2629 |
limit = total_limit; |
|
2630 |
usage = total_usage; |
|
2631 |
} |
|
2499 | 2632 |
} |
2500 |
return _.extend(value, {'name': key, 'id': key, |
|
2633 |
|
|
2634 |
var limit_active = limit; |
|
2635 |
var usage_active = usage; |
|
2636 |
|
|
2637 |
var id = uuid + ":" + key; |
|
2638 |
return _.extend(value, { |
|
2639 |
'name': key, |
|
2640 |
'id': id, |
|
2501 | 2641 |
'available': available, |
2502 | 2642 |
'available_active': available_active, |
2643 |
'total_available': total_available, |
|
2503 | 2644 |
'limit_active': limit_active, |
2645 |
'project_id': uuid, |
|
2504 | 2646 |
'usage_active': usage_active, |
2505 |
'resource': snf.storage.resources.get(key)}); |
|
2647 |
'resource': snf.storage.resources.get(key) |
|
2648 |
}); |
|
2649 |
})); |
|
2506 | 2650 |
}); |
2507 |
return filtered;
|
|
2651 |
return parsed;
|
|
2508 | 2652 |
}, |
2509 | 2653 |
|
2654 |
project_key: function(project, key) { |
|
2655 |
return project + ":" + key; |
|
2656 |
}, |
|
2657 |
|
|
2510 | 2658 |
get_by_id: function(k) { |
2511 | 2659 |
return this.filter(function(q) { return q.get('name') == k})[0] |
2512 | 2660 |
}, |
2513 | 2661 |
|
2514 | 2662 |
get_available_for_vm: function(options) { |
2515 |
var quotas = synnefo.storage.quotas;
|
|
2663 |
var quotas = this;
|
|
2516 | 2664 |
var key = 'available'; |
2517 | 2665 |
var available_quota = {}; |
2518 | 2666 |
_.each(['cyclades.ram', 'cyclades.cpu', 'cyclades.disk'], |
... | ... | |
2521 | 2669 |
available_quota[key.replace('cyclades.', '')] = value; |
2522 | 2670 |
}); |
2523 | 2671 |
return available_quota; |
2672 |
}, |
|
2673 |
|
|
2674 |
can_create: function(type) { |
|
2675 |
return this.get_available_projects(this.required_quota[type]).length > 0; |
|
2676 |
}, |
|
2677 |
|
|
2678 |
get_available_projects: function(quotas) { |
|
2679 |
return synnefo.storage.projects.filter(function(project) { |
|
2680 |
return project.quotas.can_fit(quotas); |
|
2681 |
}); |
|
2682 |
}, |
|
2683 |
|
|
2684 |
can_fit: function(quotas, total, _issues) { |
|
2685 |
var issues = []; |
|
2686 |
if (total === undefined) { total = false } |
|
2687 |
_.each(quotas, function(value, key) { |
|
2688 |
var q = this.get(key); |
|
2689 |
if (!q) { issues.push(key); return } |
|
2690 |
var quota = q.get('available_active'); |
|
2691 |
if (total) { |
|
2692 |
quota = q.get('available'); |
|
2693 |
} |
|
2694 |
if (quota < value) { |
|
2695 |
issues.push(key); |
|
2696 |
} |
|
2697 |
}, this); |
|
2698 |
if (_issues) { return issues } |
|
2699 |
return issues.length === 0; |
|
2524 | 2700 |
} |
2525 | 2701 |
}) |
2526 | 2702 |
|
... | ... | |
2533 | 2709 |
api_type: 'accounts', |
2534 | 2710 |
path: 'resources', |
2535 | 2711 |
model: models.Network, |
2712 |
display_name_map: { |
|
2713 |
'cyclades.vm': 'Machines', |
|
2714 |
'cyclades.ram': 'Memory size', |
|
2715 |
'cyclades.total_ram': 'Memory size (total)', |
|
2716 |
'cyclades.cpu': 'CPUs', |
|
2717 |
'cyclades.total_cpu': 'CPUs (total)', |
|
2718 |
'cyclades.floating_ip': 'IP Addresses', |
|
2719 |
'pithos.diskpace': 'Storage space', |
|
2720 |
'cyclades.disk': 'Disk size', |
|
2721 |
'cyclades.network.private': 'Private networks' |
|
2722 |
}, |
|
2536 | 2723 |
|
2537 | 2724 |
parse: function(resp) { |
2538 | 2725 |
return _.map(resp, function(value, key) { |
2539 |
return _.extend(value, {'name': key, 'id': key}); |
|
2540 |
}) |
|
2726 |
var display_name = this.display_name_map[key] || key; |
|
2727 |
return _.extend(value, { |
|
2728 |
'name': key, |
|
2729 |
'id': key, |
|
2730 |
'display_name': display_name |
|
2731 |
}); |
|
2732 |
}, this); |
|
2733 |
} |
|
2734 |
}); |
|
2735 |
|
|
2736 |
models.ProjectQuotas = models.Quotas.extend({}) |
|
2737 |
_.extend(models.ProjectQuotas.prototype, Backbone.FilteredCollection.prototype); |
|
2738 |
models.ProjectQuotas.prototype.get = function(key) { |
|
2739 |
key = this.project_id + ":" + key; |
|
2740 |
return models.ProjectQuotas.__super__.get.call(this, key); |
|
2741 |
} |
|
2742 |
|
|
2743 |
models.Project = models.Model.extend({ |
|
2744 |
api_type: 'accounts', |
|
2745 |
path: 'projects', |
|
2746 |
|
|
2747 |
initialize: function() { |
|
2748 |
var self = this; |
|
2749 |
this.quotas = new models.ProjectQuotas(undefined, { |
|
2750 |
collection: synnefo.storage.quotas, |
|
2751 |
collectionFilter: function(m) { |
|
2752 |
return self.id == m.get('project_id') |
|
2753 |
}}); |
|
2754 |
this.quotas.bind('change', function() { |
|
2755 |
self.trigger('change:_quotas'); |
|
2756 |
}); |
|
2757 |
this.quotas.project_id = this.id; |
|
2758 |
models.Project.__super__.initialize.apply(this, arguments); |
|
2759 |
} |
|
2760 |
}); |
|
2761 |
|
|
2762 |
models.Projects = models.Collection.extend({ |
|
2763 |
api_type: 'accounts', |
|
2764 |
path: 'projects', |
|
2765 |
model: models.Project, |
|
2766 |
supportIncUpdates: false, |
|
2767 |
user_project_uuid: null, |
|
2768 |
|
|
2769 |
url: function() { |
|
2770 |
var args = Array.prototype.splice.call(arguments, 0); |
|
2771 |
var url = models.Projects.__super__.url.apply(this, args); |
|
2772 |
return url + "?mode=member"; |
|
2773 |
}, |
|
2774 |
|
|
2775 |
parse: function(resp) { |
|
2776 |
_.each(resp, function(project){ |
|
2777 |
if (project.base_project) { |
|
2778 |
this.user_project_uuid = project.id; |
|
2779 |
} |
|
2780 |
if (project.id == synnefo.user.get_username()) { |
|
2781 |
project.name = "User project" |
|
2782 |
} |
|
2783 |
}, this); |
|
2784 |
return resp; |
|
2785 |
}, |
|
2786 |
|
|
2787 |
get_user_project: function() { |
|
2788 |
return this.get(synnefo.user.current_username); |
|
2789 |
}, |
|
2790 |
|
|
2791 |
comparator: function(project) { |
|
2792 |
if (project.get('base_project')) { return -100 } |
|
2793 |
return project.get('name'); |
|
2541 | 2794 |
} |
2542 | 2795 |
}); |
2543 | 2796 |
|
... | ... | |
2548 | 2801 |
snf.storage.keys = new models.PublicKeys(); |
2549 | 2802 |
snf.storage.resources = new models.Resources(); |
2550 | 2803 |
snf.storage.quotas = new models.Quotas(); |
2804 |
snf.storage.projects = new models.Projects(); |
|
2551 | 2805 |
snf.storage.public_pools = new models.PublicPools(); |
2552 | 2806 |
|
2553 | 2807 |
})(this); |
Also available in: Unified diff