mirror of https://github.com/rancher/ui.git
Implemented CRUD for project
This commit is contained in:
parent
e761fefd8e
commit
bb9e744ccf
|
|
@ -0,0 +1,15 @@
|
|||
import { hash } from 'rsvp';
|
||||
import { inject as service } from '@ember/service';
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend({
|
||||
authzStore: service('authz-store'),
|
||||
model: function (params) {
|
||||
return hash({
|
||||
project: this.get('authzStore').find('project', params.project_id),
|
||||
projectRoleTemplateBindings: this.get('authzStore').findAll('projectRoleTemplateBinding', null, { filter: { projectId: params.project_id } }),
|
||||
projects: this.get('authzStore').findAll('project'),
|
||||
roles: this.get('authzStore').findAll('projectRoleTemplate'),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
{{new-edit-project model=model editing=true}}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend({
|
||||
authzStore: service('authz-store'),
|
||||
model: function () {
|
||||
return this.get('authzStore').findAll('project').then(projects => {
|
||||
return {
|
||||
projects
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<section class="header clearfix">
|
||||
<h1 class="pull-left">{{t 'projectsPage.header'}}</h1>
|
||||
<div class="vertical-middle"></div>
|
||||
|
||||
<div class="right-buttons">
|
||||
{{#link-to "authenticated.projects.new" class="btn bg-primary btn-sm icon-btn"}}
|
||||
<span class="darken">
|
||||
<i class="icon icon-folder"></i>
|
||||
</span>
|
||||
<span>{{t 'projectsPage.addProject'}}</span>
|
||||
{{/link-to}}
|
||||
</div>
|
||||
</section>
|
||||
{{project-table model=model.projects}}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { hash } from 'rsvp';
|
||||
import { inject as service } from '@ember/service';
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend({
|
||||
authzStore: service('authz-store'),
|
||||
model: function () {
|
||||
const project = this.get('authzStore').createRecord({
|
||||
type: `project`,
|
||||
name: '',
|
||||
});
|
||||
const projectRoleTemplateBindings = [{
|
||||
subjectKind: 'User',
|
||||
subjectName: '',
|
||||
projectRoleTemplateId: '',
|
||||
projectId: '',
|
||||
}];
|
||||
return hash({
|
||||
project,
|
||||
projectRoleTemplateBindings,
|
||||
projects: this.get('authzStore').findAll('project'),
|
||||
roles: this.get('authzStore').findAll('projectRoleTemplate'),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
{{new-edit-project model=model editing=false}}
|
||||
|
|
@ -32,17 +32,7 @@ var Project = Resource.extend(PolledResource, {
|
|||
|
||||
actions: {
|
||||
edit: function() {
|
||||
this.get('router').transitionTo('authenticated.clusters.project', this.get('id'));
|
||||
},
|
||||
|
||||
delete: function() {
|
||||
return this.delete().then(() => {
|
||||
// If you're in the project that was deleted, go back to the default project
|
||||
if ( this.get('active') )
|
||||
{
|
||||
window.location.href = window.location.href;
|
||||
}
|
||||
});
|
||||
this.get('router').transitionTo('authenticated.projects.edit', this.get('id'));
|
||||
},
|
||||
|
||||
activate: function() {
|
||||
|
|
@ -85,14 +75,9 @@ var Project = Resource.extend(PolledResource, {
|
|||
let l = this.get('links');
|
||||
|
||||
var choices = [
|
||||
{ label: 'action.setDefault', icon: 'icon icon-star-fill', action: 'setAsDefault', enabled: this.get('canSetDefault')},
|
||||
{ label: 'action.edit', icon: 'icon icon-edit', action: 'edit', enabled: true},
|
||||
{ divider: true },
|
||||
{ label: 'action.edit', icon: 'icon icon-edit', action: 'edit', enabled: !!l.update },
|
||||
{ divider: true },
|
||||
{ label: 'action.activate', icon: 'icon icon-play', action: 'activate', enabled: !!a.activate, bulkable: true},
|
||||
{ label: 'action.deactivate', icon: 'icon icon-pause', action: 'promptStop', enabled: !!a.deactivate, altAction: 'deactivate', bulkable: true},
|
||||
{ divider: true },
|
||||
{ label: 'action.remove', icon: 'icon icon-trash', action: 'promptDelete', enabled: !!l.remove, altAction: 'delete', bulkable: true },
|
||||
{ label: 'action.remove', icon: 'icon icon-trash', action: 'promptDelete', enabled: true, altAction: 'delete', bulkable: true },
|
||||
{ divider: true },
|
||||
{ label: 'action.viewInApi', icon: 'icon icon-external-link',action: 'goToApi', enabled: true },
|
||||
];
|
||||
|
|
@ -100,6 +85,15 @@ var Project = Resource.extend(PolledResource, {
|
|||
return choices;
|
||||
}),
|
||||
|
||||
delete: function (/*arguments*/) {
|
||||
var promise = this._super.apply(this, arguments);
|
||||
return promise.then(() => {
|
||||
this.set('state', 'removed');
|
||||
}).catch((err) => {
|
||||
this.get('growl').fromError('Error deleting', err);
|
||||
});
|
||||
},
|
||||
|
||||
icon: computed('active', function() {
|
||||
if ( this.get('active') )
|
||||
{
|
||||
|
|
|
|||
|
|
@ -37,6 +37,12 @@ Router.map(function() {
|
|||
|
||||
this.route('prefs');
|
||||
|
||||
this.route('projects', {path: '/projects'}, function() {
|
||||
this.route('index', {path: '/'});
|
||||
this.route('edit', {path: '/:project_id'});
|
||||
this.route('new', {path: '/add'});
|
||||
});
|
||||
|
||||
// Clusters
|
||||
this.route('clusters', {path: '/clusters'}, function() {
|
||||
this.route('index', {path: '/'});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
import { alias } from '@ember/object/computed';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { reject, all as PromiseAll } from 'rsvp';
|
||||
import Component from '@ember/component';
|
||||
import NewOrEdit from 'ui/mixins/new-or-edit';
|
||||
import layout from './template';
|
||||
|
||||
export default Component.extend(NewOrEdit, {
|
||||
layout,
|
||||
intl: service(),
|
||||
router: service(),
|
||||
authzStore: service('authz-store'),
|
||||
model: null,
|
||||
|
||||
primaryResource: alias('model.project'),
|
||||
memberArray: alias('model.projectRoleTemplateBindings'),
|
||||
|
||||
actions: {
|
||||
cancel() {
|
||||
this.goBack();
|
||||
},
|
||||
addMember(kind) {
|
||||
this.get('memberArray').pushObject({
|
||||
subjectKind: kind,
|
||||
subjectName: '',
|
||||
projectRoleTemplateId: '',
|
||||
projectId: '',
|
||||
});
|
||||
},
|
||||
removeMember(obj) {
|
||||
this.get('memberArray').removeObject(obj);
|
||||
},
|
||||
},
|
||||
|
||||
goBack: function () {
|
||||
this.get('router').transitionTo('/projects');
|
||||
},
|
||||
|
||||
doesNameExist() {
|
||||
const project = this.get('primaryResource');
|
||||
const currentProjects = this.get('model.projects');
|
||||
|
||||
if (currentProjects.findBy('name', project.get('name'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
doseMemberNameInvalid() {
|
||||
const members = this.get('memberArray');
|
||||
return members.any(r => r.subjectName.length === 0);
|
||||
},
|
||||
|
||||
doseMemberRoleInvalid() {
|
||||
const members = this.get('memberArray');
|
||||
return members.any(r => r.projectRoleTemplateId.length === 0);
|
||||
},
|
||||
|
||||
validate: function () {
|
||||
var errors = this.get('errors', errors) || [];
|
||||
|
||||
if ((this.get('model.project.name') || '').trim().length === 0) {
|
||||
errors.push(this.get('intl').findTranslationByKey('projectsPage.new.errors.nameReq'));
|
||||
}
|
||||
|
||||
if (!this.get('editing') && this.doesNameExist()) {
|
||||
errors.push(this.get('intl').findTranslationByKey('projectsPage.new.errors.nameInExists'));
|
||||
}
|
||||
|
||||
if (this.doseMemberNameInvalid()) {
|
||||
errors.push(this.get('intl').findTranslationByKey('projectsPage.new.errors.memberNameReq'));
|
||||
}
|
||||
|
||||
if (this.doseMemberRoleInvalid()) {
|
||||
errors.push(this.get('intl').findTranslationByKey('projectsPage.new.errors.memberRoleReq'));
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
this.set('errors', errors.uniq());
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
this.set('errors', null);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
doSave() {
|
||||
return this._super.apply(this, arguments).then((project) => {
|
||||
const projectId = project.id;
|
||||
const members = this.get('memberArray');
|
||||
const promises = [];
|
||||
members.forEach(member => {
|
||||
member.projectId = projectId;
|
||||
const promise = this.get('authzStore').rawRequest({
|
||||
url: 'projectroletemplatebinding',
|
||||
method: 'POST',
|
||||
data: member,
|
||||
});
|
||||
promises.push(promise);
|
||||
});
|
||||
return PromiseAll(promises).catch((error) => {
|
||||
return reject(error.body.message);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
doneSaving() {
|
||||
this.goBack();
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<section class="header clearfix">
|
||||
<div class="pull-left">
|
||||
{{#if editing}}
|
||||
<h1>{{t 'projectsPage.editProject'}}</h1>
|
||||
{{else}}
|
||||
<h1>{{t 'projectsPage.addProject'}}</h1>
|
||||
{{/if}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="horizontal-form container-fluid">
|
||||
{{form-name-description
|
||||
name=model.project.name
|
||||
nameLabel="projectsPage.new.form.name.label"
|
||||
nameRequired=true
|
||||
nameDisabled=editing
|
||||
namePlaceholder="projectsPage.new.form.name.placeholder"
|
||||
colClass="col span-7"
|
||||
descriptionShown=false
|
||||
}}
|
||||
</section>
|
||||
|
||||
<section class="horizontal-form container-fluid">
|
||||
<div class="row">
|
||||
<div class="col span-3">
|
||||
<label class="pb-5 acc-label">{{t 'projectsPage.new.form.members.labelText'}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-10 pr-10">
|
||||
<table class="table fixed no-lines">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="150">{{t 'projectsPage.new.form.members.kind.label'}}</th>
|
||||
<th>{{t 'projectsPage.new.form.members.name.label'}}</th>
|
||||
<th>{{t 'projectsPage.new.form.members.role.label'}}</th>
|
||||
<th width="10"> </th>
|
||||
<th width="40"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each memberArray as |member|}}
|
||||
{{project-member-row member=member roles=model.roles remove="removeMember"}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<button class="btn bg-link icon-btn p-0" {{action "addMember" "User"}}>
|
||||
<span class="darken"><i class="icon icon-plus text-small"/></span>
|
||||
<span>{{t 'projectsPage.new.form.members.addUser'}}</span>
|
||||
</button>
|
||||
|
||||
<button class="btn bg-link icon-btn p-0" {{action "addMember" "Group"}}>
|
||||
<span class="darken"><i class="icon icon-plus text-small"/></span>
|
||||
<span>{{t 'projectsPage.new.form.members.addGroup'}}</span>
|
||||
</button>
|
||||
|
||||
<button class="btn bg-link icon-btn p-0" {{action "addMember" "ServiceAccount"}}>
|
||||
<span class="darken"><i class="icon icon-plus text-small"/></span>
|
||||
<span>{{t 'projectsPage.new.form.members.addServiceAccount'}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{top-errors errors=errors}}
|
||||
{{save-cancel createLabel=(if editing 'projectsPage.saveEdit' 'projectsPage.saveNew') save="save" cancel="cancel"}}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import Component from '@ember/component';
|
||||
import layout from './template';
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
member: null,
|
||||
roles: null,
|
||||
|
||||
tagName: 'TR',
|
||||
classNames: 'main-row',
|
||||
|
||||
actions: {
|
||||
remove: function () {
|
||||
this.sendAction('remove', this.get('member'));
|
||||
}
|
||||
},
|
||||
|
||||
init: function () {
|
||||
this._super(...arguments);
|
||||
this.set('choices', this.get('roles').map(role => {
|
||||
return {
|
||||
label: role.name,
|
||||
value: role.id,
|
||||
};
|
||||
}));
|
||||
},
|
||||
|
||||
kind: function() {
|
||||
return `projectsPage.new.form.members.${this.get('member.subjectKind').toLowerCase()}`
|
||||
}.property('member.subjectKind')
|
||||
});
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<td class="pr-20">
|
||||
{{t kind}}
|
||||
</td>
|
||||
<td class="pr-20">
|
||||
{{input type="text" value=member.subjectName classNames="form-control"}}
|
||||
</td>
|
||||
<td class="pr-20">
|
||||
{{searchable-select content=choices value=member.projectRoleTemplateId}}
|
||||
</td>
|
||||
<td> </td>
|
||||
<div class="input-group-btn">
|
||||
<button class="btn bg-primary btn-sm" {{action "remove"}}>
|
||||
<i class="icon icon-minus" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -1,18 +1,8 @@
|
|||
import Component from '@ember/component';
|
||||
import layout from './template';
|
||||
import { inject as service } from '@ember/service'
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
model: null,
|
||||
tagName: 'TR',
|
||||
showCluster: false,
|
||||
scope: service(),
|
||||
|
||||
actions: {
|
||||
switchTo(id) {
|
||||
// @TODO bad
|
||||
window.lc('authenticated').send('switchProject', id);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,36 +1,12 @@
|
|||
{{#if bulkActions}}
|
||||
<td valign="middle" class="row-check" style="padding-top: 2px;">
|
||||
{{check-box nodeId=model.id}}
|
||||
</td>
|
||||
{{/if}}
|
||||
<td data-title="{{dt.state}}" class="state">
|
||||
{{badge-state model=model}}
|
||||
<td valign="middle" class="row-check" style="padding-top: 2px;">
|
||||
|
||||
</td>
|
||||
{{#if showCluster}}
|
||||
<td data-title="{{dt.cluster}}">
|
||||
<a href="{{href-to 'authenticated.clusters.cluster.index' model.cluster.id}}">{{model.cluster.displayName}}</a>
|
||||
</td>
|
||||
{{/if}}
|
||||
<td data-title="{{dt.name}}">
|
||||
<a href="{{href-to 'authenticated.project.index' model.id}}" {{action "switchTo" model.id}}>{{model.displayName}}</a>
|
||||
<td data-title="{{t 'projects.table.project.label'}}:" class="clip">
|
||||
{{#link-to "authenticated.projects.edit" model.id }} {{model.name}} {{/link-to}}
|
||||
</td>
|
||||
<td colspan="3" class="text-center text-muted">
|
||||
Counts Coming Soon
|
||||
<td data-title="{{t 'projects.table.created.label'}}:">
|
||||
{{date-calendar model.created}}
|
||||
</td>
|
||||
<!--
|
||||
<td data-title="{{dt.stacks}}" class="text-center">
|
||||
{{model.numStacks}}
|
||||
</td>
|
||||
<td data-title="{{dt.services}}" class="text-center">
|
||||
{{model.numServices}}
|
||||
</td>
|
||||
<td data-title="{{dt.containers}}" class="text-center">
|
||||
{{model.numContainers}}
|
||||
</td>
|
||||
-->
|
||||
<td data-title="{{dt.default}}" class="text-center">
|
||||
{{#if model.isDefault}}<i class="icon icon-star-fill"></i>{{else}}<span class="text-muted">–</span>{{/if}}
|
||||
</td>
|
||||
<td data-title="{{dt.actions}} "class="actions">
|
||||
<td data-title="{{t 'generic.actions'}}:" class="actions">
|
||||
{{action-menu model=model}}
|
||||
</td>
|
||||
</td>
|
||||
|
|
@ -1,70 +1,21 @@
|
|||
import { computed } from '@ember/object';
|
||||
import Component from '@ember/component';
|
||||
import layout from './template';
|
||||
|
||||
const headersWithCluster = [
|
||||
{
|
||||
name: 'state',
|
||||
sort: ['sortState','name','id'],
|
||||
translationKey: 'generic.state',
|
||||
width: 125,
|
||||
},
|
||||
{
|
||||
name: 'cluster',
|
||||
sort: ['cluster.displayName','displayName','id'],
|
||||
translationKey: 'clustersPage.cluster.label',
|
||||
searchField: ['cluster.displayName'],
|
||||
},
|
||||
{
|
||||
const headers = [{
|
||||
name: 'name',
|
||||
sort: ['displayName','id'],
|
||||
translationKey: 'clustersPage.environment.label',
|
||||
searchField: ['displayName'],
|
||||
},
|
||||
{
|
||||
name: 'stacks',
|
||||
sort: ['numStacks','name','id'],
|
||||
translationKey: 'generic.stacks',
|
||||
width: 100,
|
||||
classNames: 'text-center',
|
||||
},
|
||||
{
|
||||
name: 'services',
|
||||
sort: ['numServices','name','id'],
|
||||
translationKey: 'generic.services',
|
||||
width: 100,
|
||||
classNames: 'text-center',
|
||||
},
|
||||
{
|
||||
name: 'containers',
|
||||
sort: ['numContainers','name','id'],
|
||||
translationKey: 'generic.containers',
|
||||
width: 120,
|
||||
classNames: 'text-center',
|
||||
},
|
||||
{
|
||||
name: 'default',
|
||||
sort: false,
|
||||
translationKey: 'clusterRow.loginDefault',
|
||||
width: 60,
|
||||
classNames: 'text-center',
|
||||
sort: ['name'],
|
||||
translationKey: 'projectsPage.table.header.project.label',
|
||||
},{
|
||||
name: 'created',
|
||||
sort: ['created'],
|
||||
translationKey: 'projectsPage.table.header.created.label',
|
||||
width: '125',
|
||||
},
|
||||
];
|
||||
|
||||
const headersWithoutCluster = headersWithCluster.filter(x => x.name !== 'cluster');
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
tagName: '',
|
||||
showCluster: false,
|
||||
bulkActions: true,
|
||||
search: true,
|
||||
|
||||
headers: computed('showCluster', function() {
|
||||
if ( this.get('showCluster') ) {
|
||||
return headersWithCluster;
|
||||
} else {
|
||||
return headersWithoutCluster;
|
||||
}
|
||||
}),
|
||||
headers,
|
||||
sortBy: 'name',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,22 +1,18 @@
|
|||
{{#sortable-table
|
||||
tableClassNames="bordered"
|
||||
bulkActions=bulkActions
|
||||
paging=true
|
||||
pagingLabel="pagination.project"
|
||||
search=search
|
||||
sortBy=sortBy
|
||||
descending=descending
|
||||
headers=headers
|
||||
body=model
|
||||
sortBy=sortBy
|
||||
fullRows=true
|
||||
stickyHeader=false
|
||||
as |sortable kind p dt|
|
||||
}}
|
||||
{{#if (eq kind "row")}}
|
||||
{{project-row model=p dt=dt showCluster=showCluster search=search bulkActions=bulkActions}}
|
||||
{{project-row model=p dt=dt}}
|
||||
{{else if (eq kind "nomatch")}}
|
||||
<tr><td colspan="{{sortable.fullColspan}}" class="text-center text-muted lacsso pt-20 pb-20">{{t 'clusterRow.noMatch'}}</td></tr>
|
||||
<tr><td colspan="{{sortable.fullColspan}}" class="text-center text-muted lacsso pt-20 pb-20">{{t 'projectsPage.table.noMatch'}}</td></tr>
|
||||
{{else if (eq kind "norows")}}
|
||||
<tr><td colspan="{{sortable.fullColspan}}" class="text-center text-muted lacsso pt-20 pb-20">{{t 'clusterRow.noData'}}</td></tr>
|
||||
<tr><td colspan="{{sortable.fullColspan}}" class="text-center text-muted lacsso pt-20 pb-20">{{t 'projectsPage.table.noData'}}</td></tr>
|
||||
{{/if}}
|
||||
{{/sortable-table}}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@ export const getClusterId = function() { return this.get('clusterId'); };
|
|||
},
|
||||
*/
|
||||
const navTree = [
|
||||
{
|
||||
route: 'authenticated.projects.index',
|
||||
localizedLabel: 'projectsPage.header'
|
||||
}
|
||||
];
|
||||
|
||||
export function addItem(opt) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export { default } from 'shared/components/new-edit-project/component';
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from 'shared/components/project-member-row/component';
|
||||
|
|
@ -497,6 +497,45 @@ certificatesPage:
|
|||
description:
|
||||
placeholder: e.g. EV cert for mydomain.com
|
||||
|
||||
projectsPage:
|
||||
header: Projects
|
||||
addProject: Add Project
|
||||
editProject: Edit Project
|
||||
saveEdit: Edit
|
||||
saveNew: Create
|
||||
table:
|
||||
noMatch: No projects match the current search.
|
||||
noData: This cluster doesn't have any projects yet.
|
||||
header:
|
||||
project:
|
||||
label: Project Name
|
||||
created:
|
||||
label: Created
|
||||
new:
|
||||
form:
|
||||
name:
|
||||
placeholder: e.g. lab
|
||||
label: Project Name
|
||||
members:
|
||||
user: User
|
||||
group: Group
|
||||
serviceaccount: Service Account
|
||||
labelText: Members
|
||||
addUser: Add User
|
||||
addGroup: Add Group
|
||||
addServiceAccount: Add Service Account
|
||||
kind:
|
||||
label: Kind
|
||||
name:
|
||||
label: Name
|
||||
role:
|
||||
label: Role
|
||||
errors:
|
||||
nameReq: Name is requried.
|
||||
nameInExists: Name is already exists. Please use a new project name.
|
||||
memberNameReq: Name is requried for a member
|
||||
memberRoleReq: Role is requried for a member
|
||||
|
||||
clustersPage:
|
||||
header: Clusters
|
||||
newCluster: Add Cluster
|
||||
|
|
|
|||
Loading…
Reference in New Issue