mirror of https://github.com/goharbor/harbor.git
Refactor Harbor Clarity integration code structure.
This commit is contained in:
commit
23f0ff1ea5
|
@ -0,0 +1,16 @@
|
|||
FROM node:7.4.0
|
||||
|
||||
RUN mkdir -p /usr/src/app
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY harbor-app /usr/src/app
|
||||
|
||||
|
||||
RUN npm install -g bower
|
||||
RUN npm install -g angular-cli
|
||||
RUN npm install
|
||||
|
||||
EXPOSE 4200
|
||||
|
||||
ENTRYPOINT [ "npm", "start" ]
|
|
@ -0,0 +1,20 @@
|
|||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
|
||||
[*.md]
|
||||
max_line_length = 0
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# Indentation override
|
||||
#[lib/**.js]
|
||||
#[{package.json,.travis.yml}]
|
||||
#[**/**.js]
|
|
@ -0,0 +1,9 @@
|
|||
coverage/
|
||||
dist/
|
||||
html-report/
|
||||
node_modules/
|
||||
typings/
|
||||
**/*npm-debug.log.*
|
||||
**/*yarn-error.log.*
|
||||
.idea/
|
||||
.DS_Store
|
|
@ -0,0 +1,10 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- "6.0"
|
||||
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
|
@ -0,0 +1,19 @@
|
|||
Contributor Code of Conduct
|
||||
======================
|
||||
|
||||
As contributors and maintainers of the Clarity project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities.
|
||||
|
||||
Communication through any of Clarity's channels (GitHub, mailing lists, Twitter, and so on) must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
||||
|
||||
We promise to extend courtesy and respect to everyone involved in this project, regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to the Clarity project to do the same.
|
||||
|
||||
If any member of the community violates this code of conduct, the maintainers of the Clarity project may take action, including removing issues, comments, and PRs or blocking accounts, as deemed appropriate.
|
||||
|
||||
If you are subjected to or witness unacceptable behavior, or have any other concerns, please communicate with us.
|
||||
|
||||
If you have suggestions to improve this Code of Conduct, please submit an issue or PR.
|
||||
|
||||
|
||||
**Attribution**
|
||||
|
||||
This Code of Conduct is adapted from the Angular project, version 0.3a-angular, available at this page: https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md
|
|
@ -0,0 +1,113 @@
|
|||
|
||||
|
||||
# Contributing to clarity-seed
|
||||
|
||||
The clarity-seed project team welcomes contributions from the community. Follow the guidelines to contribute to the seed.
|
||||
|
||||
## Contribution Guidelines
|
||||
|
||||
Before you start working with Clarity, please complete the following steps:
|
||||
|
||||
- Read our [code of conduct](/CODE_OF_CONDUCT.md).
|
||||
- Read our [Developer Certificate of Origin](https://cla.vmware.com/dco). All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch.
|
||||
|
||||
## Contribution Flow
|
||||
|
||||
Here are the typical steps in a contributor's workflow:
|
||||
|
||||
- [Fork](https://help.github.com/articles/fork-a-repo/) the main Clarity seed repository.
|
||||
- Clone your fork and set the upstream remote to the main Clarity repository.
|
||||
- Set your name and e-mail in the Git configuration for signing.
|
||||
- Create a topic branch from where you want to base your work.
|
||||
- Make commits of logical units.
|
||||
- Make sure your commit messages are in the proper format (see below).
|
||||
- Push your changes to a topic branch in your fork of the repository.
|
||||
- [Submit a pull request](https://help.github.com/articles/about-pull-requests/).
|
||||
|
||||
Example:
|
||||
|
||||
``` shell
|
||||
# Clone your forked repository
|
||||
git clone git@github.com:<github username>/clarity-seed.git
|
||||
|
||||
# Navigate to the directory
|
||||
cd clarity-seed
|
||||
|
||||
# Set name and e-mail configuration
|
||||
git config user.name "John Doe"
|
||||
git config user.email johndoe@example.com
|
||||
|
||||
# Setup the upstream remote
|
||||
git remote add upstream https://github.com/vmware/clarity-seed.git
|
||||
|
||||
# Create a topic branch for your changes
|
||||
git checkout -b my-new-feature master
|
||||
|
||||
# After making the desired changes, commit and push to your fork
|
||||
git commit -a -s
|
||||
git push origin my-new-feature
|
||||
```
|
||||
|
||||
### Staying In Sync With Upstream
|
||||
|
||||
When your branch gets out of sync with the master branch, use the following to update:
|
||||
|
||||
``` shell
|
||||
git checkout my-new-feature
|
||||
git fetch -a
|
||||
git pull --rebase upstream master
|
||||
git push --force-with-lease origin my-new-feature
|
||||
```
|
||||
|
||||
### Updating Pull Requests
|
||||
|
||||
If your PR fails to pass CI, or requires changes based on code review, you'll most likely want to squash these changes into existing commits.
|
||||
|
||||
If your pull request contains a single commit, or your changes are related to the most recent commit, you can amend the commit.
|
||||
|
||||
``` shell
|
||||
git add .
|
||||
git commit --amend
|
||||
git push --force-with-lease origin my-new-feature
|
||||
```
|
||||
|
||||
If you need to squash changes into an earlier commit, use the following:
|
||||
|
||||
``` shell
|
||||
git add .
|
||||
git commit --fixup <commit>
|
||||
git rebase -i --autosquash master
|
||||
git push --force-with-lease origin my-new-feature
|
||||
```
|
||||
|
||||
Make sure you add a comment to the PR indicating that your changes are ready to review. GitHub does not generate a notification when you use git push.
|
||||
|
||||
### Formatting Commit Messages
|
||||
|
||||
Use this format for your commit message:
|
||||
|
||||
```
|
||||
<detailed commit message>
|
||||
<BLANK LINE>
|
||||
<reference to closing an issue>
|
||||
<BLANK LINE>
|
||||
Signed-off-by: Your Name <your.email@example.com>
|
||||
```
|
||||
|
||||
#### Writing Guidelines
|
||||
|
||||
These documents provide guidance creating a well-crafted commit message:
|
||||
|
||||
* [How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/)
|
||||
* [Closing Issues Via Commit Messages](https://help.github.com/articles/closing-issues-via-commit-messages/)
|
||||
|
||||
## Reporting Bugs and Creating Issues
|
||||
|
||||
You can submit an issue or a bug to our [GitHub repository](https://github.com/vmware/clarity-seed/issues). You must provide:
|
||||
|
||||
* Instruction on how to replicate the issue
|
||||
* The version number of Angular
|
||||
* The version number of Clarity
|
||||
* The version number of Node
|
||||
* The browser name and version number
|
||||
* The OS running the seed
|
|
@ -0,0 +1,16 @@
|
|||
Clarity Seed
|
||||
|
||||
Copyright © 2016 VMware, Inc. All rights reserved
|
||||
|
||||
The MIT license (the “License”) set forth below applies to all parts of the Clarity Seed project. You may not use this file except in compliance with the License.
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
Clarity Seed
|
||||
|
||||
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
|
||||
|
||||
This product is licensed to you under the MIT license (the "MIT License"). You may not use this product except in compliance with the MIT License.
|
||||
|
||||
This product may include a number of subcomponents with separate copyright notices and license terms. Your use of these subcomponents is subject to the terms and conditions of the subcomponent's license, as noted in the LICENSE file.
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
![Clarity](logo.png)
|
||||
|
||||
Clarity Seed
|
||||
============
|
||||
This is a seed project for Angular 2 applications using [Clarity](https://github.com/vmware/clarity). For more information on the Clarity Design System, visit the [Clarity website](https://vmware.github.io/clarity/).
|
||||
|
||||
We offer this seed project in three different build systems:
|
||||
|
||||
1. **Angular-CLI version (branch: master)**
|
||||
|
||||
2. Webpack 2 version (branch: webpack)
|
||||
|
||||
3. SystemJS version (branch: systemjs)
|
||||
|
||||
Getting started
|
||||
----------------------------------
|
||||
|
||||
#### Angular-CLI version
|
||||
|
||||
This seed version provides the following out of the box:
|
||||
|
||||
- Angular 2 application with [clarity-icons](https://www.npmjs.com/package/clarity-icons), [clarity-ui](https://www.npmjs.com/package/clarity-ui) and [clarity-angular](https://www.npmjs.com/package/clarity-angular) included
|
||||
- Development and production builds
|
||||
- Unit test setup with Jasmine and Karma
|
||||
- End-to-end test setup with Protractor
|
||||
- SASS processor
|
||||
- TSLint
|
||||
- And other goodies that come with [Angular-CLI](https://github.com/angular/angular-cli#generating-and-serving-an-angular2-project-via-a-development-server) (v1.0.0-beta.20-4)
|
||||
|
||||
#### Installation
|
||||
*Prerequisite*: Please install Angular-CLI by following [these instructions](https://github.com/angular/angular-cli#installation).
|
||||
*Note*: Even though it's optional, we recommend you to use [yarn](https://yarnpkg.com/) instead of `npm install` for installing the dependencies.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/vmware/clarity-seed.git
|
||||
cd clarity-seed
|
||||
|
||||
# install the project's dependencies
|
||||
yarn # or run "npm install"
|
||||
|
||||
# starts the application in dev mode and watches your files for livereload
|
||||
ng serve
|
||||
```
|
||||
|
||||
#### Using Angular-CLI
|
||||
```bash
|
||||
# generating a new component
|
||||
ng g component my-new-component
|
||||
|
||||
# generating a new directive
|
||||
ng g directive my-new-directive
|
||||
|
||||
# to learn more about Angular-CLI commands and their usages
|
||||
ng help
|
||||
```
|
||||
|
||||
For comprehensive documentation on Angular-CLI, please see their [github repository](https://github.com/angular/angular-cli).
|
||||
|
||||
#### Test and build scripts
|
||||
|
||||
```bash
|
||||
# running unit tests
|
||||
ng test
|
||||
|
||||
# running e2e tests
|
||||
ng e2e
|
||||
|
||||
# dev build
|
||||
ng build
|
||||
|
||||
# prod build
|
||||
ng build --prod
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
For documentation on the Clarity Design System, including a list of components and example usage, see [our website](https://vmware.github.io/clarity).
|
||||
|
||||
|
||||
#### Directory structure
|
||||
```
|
||||
.
|
||||
├── README.md
|
||||
|
||||
├── karma.conf.js <- configuration of the test runner
|
||||
├── package.json <- dependencies of the project
|
||||
├── protractor.config.js <- e2e tests configuration
|
||||
├── src/ <- source code of the application
|
||||
│ ├── app/
|
||||
│ │ └── component/
|
||||
│ │ └── <component>.component.html
|
||||
│ │ └── <component>.component.scss
|
||||
│ │ └── <component>.component.spec.ts
|
||||
│ │ └── <component>.component.ts
|
||||
│ │ └── app.component.html
|
||||
│ │ └── app.component.scss
|
||||
│ │ └── app.component.ts
|
||||
│ │ └── app.e2e-spec.js <- sample e2e spec file
|
||||
│ │ └── app.module.ts
|
||||
│ │ └── app.routing.ts
|
||||
│ │ └── main.ts <- boostrap file for the angular app
|
||||
│ └── index.html
|
||||
├── angular-cli.json <- configuration of the angular-cli
|
||||
├── tsconfig.json <- configuration of the typescript project
|
||||
├── tslint.json <- sample configuration file for tslint
|
||||
└── yarn.lock
|
||||
```
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
The Clarity project team welcomes contributions from the community. For more detailed information, see [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
The clarity-seed project is licensed under the MIT license.
|
||||
|
||||
## Feedback
|
||||
|
||||
If you find a bug or want to request a new feature, please open a [GitHub issue](https://github.com/vmware/clarity-seed/issues).
|
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"project": {
|
||||
"version": "1.0.0-beta.20-4",
|
||||
"name": "clarity-seed"
|
||||
},
|
||||
"apps": [
|
||||
{
|
||||
"root": "src",
|
||||
"outDir": "dist",
|
||||
"assets": [
|
||||
"images",
|
||||
"favicon.ico"
|
||||
],
|
||||
"index": "index.html",
|
||||
"main": "main.ts",
|
||||
"test": "test.ts",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"prefix": "app",
|
||||
"mobile": false,
|
||||
"styles": [
|
||||
"../node_modules/clarity-icons/clarity-icons.min.css",
|
||||
"../node_modules/clarity-ui/clarity-ui.min.css",
|
||||
"styles.css"
|
||||
],
|
||||
"scripts": [
|
||||
"../node_modules/core-js/client/shim.min.js",
|
||||
"../node_modules/mutationobserver-shim/dist/mutationobserver.min.js",
|
||||
"../node_modules/@webcomponents/custom-elements/custom-elements.min.js",
|
||||
"../node_modules/clarity-icons/clarity-icons.min.js",
|
||||
"../node_modules/web-animations-js/web-animations.min.js"
|
||||
],
|
||||
"environments": {
|
||||
"source": "environments/environment.ts",
|
||||
"dev": "environments/environment.ts",
|
||||
"prod": "environments/environment.prod.ts"
|
||||
}
|
||||
}
|
||||
],
|
||||
"addons": [],
|
||||
"packages": [],
|
||||
"e2e": {
|
||||
"protractor": {
|
||||
"config": "./protractor.config.js"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"karma": {
|
||||
"config": "./karma.conf.js"
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
"styleExt": "scss",
|
||||
"prefixInterfaces": false,
|
||||
"inline": {
|
||||
"style": false,
|
||||
"template": false
|
||||
},
|
||||
"spec": {
|
||||
"class": false,
|
||||
"component": true,
|
||||
"directive": true,
|
||||
"module": false,
|
||||
"pipe": true,
|
||||
"service": true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import {ClaritySeedAppHome} from './app.po';
|
||||
|
||||
fdescribe('clarity-seed app', function () {
|
||||
|
||||
let expectedMsg: string = 'This is a Clarity seed application. This is the default page that loads for the application.';
|
||||
|
||||
let page: ClaritySeedAppHome;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new ClaritySeedAppHome();
|
||||
});
|
||||
|
||||
it('should display: ' + expectedMsg, () => {
|
||||
page.navigateTo();
|
||||
expect(page.getParagraphText()).toEqual(expectedMsg)
|
||||
});
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
import { browser, element, by } from 'protractor';
|
||||
|
||||
|
||||
export class ClaritySeedAppHome {
|
||||
|
||||
navigateTo() {
|
||||
return browser.get('/');
|
||||
}
|
||||
|
||||
getParagraphText() {
|
||||
return element(by.css('my-app p')).getText();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"rootDir": "../",
|
||||
"baseUrl": "",
|
||||
"declaration": false,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "dist/out-tsc-e2e",
|
||||
"sourceMap": true,
|
||||
"target": "es5",
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
],
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/0.13/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', 'angular-cli'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-phantomjs-launcher'),
|
||||
require('karma-mocha-reporter'),
|
||||
require('karma-remap-istanbul'),
|
||||
require('angular-cli/plugins/karma')
|
||||
],
|
||||
files: [
|
||||
{pattern: './src/test.ts', watched: false}
|
||||
],
|
||||
preprocessors: {
|
||||
'./src/test.ts': ['angular-cli']
|
||||
},
|
||||
mime: {
|
||||
'text/x-typescript': ['ts', 'tsx']
|
||||
},
|
||||
remapIstanbulReporter: {
|
||||
reports: {
|
||||
html: 'coverage',
|
||||
lcovonly: './coverage/coverage.lcov'
|
||||
}
|
||||
},
|
||||
angularCli: {
|
||||
config: './angular-cli.json',
|
||||
environment: 'dev'
|
||||
},
|
||||
reporters: config.angularCli && config.angularCli.codeCoverage
|
||||
? ['mocha', 'karma-remap-istanbul']
|
||||
: ['mocha'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['PhantomJS'],
|
||||
singleRun: true
|
||||
});
|
||||
};
|
Binary file not shown.
After Width: | Height: | Size: 5.4 KiB |
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"name": "clarity-seed",
|
||||
"version": "0.8.0",
|
||||
"description": "Angular-CLI starter for a Clarity project",
|
||||
"angular-cli": {},
|
||||
"scripts": {
|
||||
"start": "ng serve --host 0.0.0.0 --proxy-config proxy.config.json",
|
||||
"lint": "tslint \"src/**/*.ts\"",
|
||||
"test": "ng test --single-run",
|
||||
"pree2e": "webdriver-manager update",
|
||||
"e2e": "protractor"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "^2.4.1",
|
||||
"@angular/compiler": "^2.4.1",
|
||||
"@angular/core": "^2.4.1",
|
||||
"@angular/forms": "^2.4.1",
|
||||
"@angular/http": "^2.4.1",
|
||||
"@angular/platform-browser": "^2.4.1",
|
||||
"@angular/platform-browser-dynamic": "^2.4.1",
|
||||
"@angular/router": "^3.4.1",
|
||||
"@webcomponents/custom-elements": "1.0.0-alpha.3",
|
||||
"clarity-angular": "^0.8.0",
|
||||
"clarity-icons": "^0.8.0",
|
||||
"clarity-ui": "^0.8.0",
|
||||
"core-js": "^2.4.1",
|
||||
"mutationobserver-shim": "^0.3.2",
|
||||
"rxjs": "^5.0.1",
|
||||
"ts-helpers": "^1.1.1",
|
||||
"web-animations-js": "^2.2.1",
|
||||
"zone.js": "^0.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/compiler-cli": "^2.4.1",
|
||||
"@types/core-js": "^0.9.34",
|
||||
"@types/jasmine": "^2.2.30",
|
||||
"@types/node": "^6.0.42",
|
||||
"angular-cli": "^1.0.0-beta.24",
|
||||
"bootstrap": "4.0.0-alpha.5",
|
||||
"codelyzer": "~1.0.0-beta.3",
|
||||
"enhanced-resolve": "^3.0.0",
|
||||
"jasmine-core": "2.4.1",
|
||||
"jasmine-spec-reporter": "2.5.0",
|
||||
"karma": "1.2.0",
|
||||
"karma-cli": "^1.0.1",
|
||||
"karma-jasmine": "^1.0.2",
|
||||
"karma-mocha-reporter": "^2.2.1",
|
||||
"karma-phantomjs-launcher": "^1.0.0",
|
||||
"karma-remap-istanbul": "^0.2.1",
|
||||
"protractor": "4.0.9",
|
||||
"ts-node": "1.2.1",
|
||||
"tslint": "^4.1.1",
|
||||
"typescript": "~2.0.3",
|
||||
"typings": "^1.4.0",
|
||||
"webdriver-manager": "10.2.5"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/docs/referenceConf.js
|
||||
|
||||
/*global jasmine */
|
||||
var SpecReporter = require('jasmine-spec-reporter');
|
||||
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./e2e/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
'browserName': 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
useAllAngular2AppRoots: true,
|
||||
beforeLaunch: function() {
|
||||
require('ts-node').register({
|
||||
project: 'e2e'
|
||||
});
|
||||
},
|
||||
onPrepare: function() {
|
||||
jasmine.getEnv().addReporter(new SpecReporter());
|
||||
}
|
||||
};
|
|
@ -0,0 +1,55 @@
|
|||
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalSize]="'lg'">
|
||||
<h3 class="modal-title">User Profile</h3>
|
||||
<div class="modal-body" style="overflow-y: hidden;">
|
||||
<form #accountSettingsFrom="ngForm" class="form">
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<label for="account_settings_username" class="col-md-4">Username</label>
|
||||
<input type="text" name="account_settings_username" [(ngModel)]="account.username" disabled id="account_settings_username" size="51">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="account_settings_email" class="col-md-4 required">Email</label>
|
||||
<label for="account_settings_email" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-right" [class.invalid]="eamilInput.invalid && (eamilInput.dirty || eamilInput.touched)">
|
||||
<input name="account_settings_email" type="text" #eamilInput="ngModel" [(ngModel)]="account.email"
|
||||
required
|
||||
pattern='^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$' id="account_settings_email" size="48">
|
||||
<span class="tooltip-content">
|
||||
Email should be a valid email address like name@example.com
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="account_settings_full_name" class="col-md-4 required">Full name</label>
|
||||
<label for="account_settings_email" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-right" [class.invalid]="fullNameInput.invalid && (fullNameInput.dirty || fullNameInput.touched)">
|
||||
<input type="text" name="account_settings_full_name" #fullNameInput="ngModel" [(ngModel)]="account.realname" required maxLengthExt="20" id="account_settings_full_name" size="48">
|
||||
<span class="tooltip-content">
|
||||
Max length of full name is 20
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="account_settings_comments" class="col-md-4">Comments</label>
|
||||
<label for="account_settings_comments" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-right" [class.invalid]="commentInput.invalid && (commentInput.dirty || commentInput.touched)">
|
||||
<input type="text" #commentInput="ngModel" maxLengthExt="20" name="account_settings_comments" [(ngModel)]="account.comment" id="account_settings_comments" size="48">
|
||||
<span class="tooltip-content">
|
||||
Length of comment should be less than 20
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
<div style="height: 30px;"></div>
|
||||
<clr-alert [clrAlertType]="'alert-danger'" [clrAlertClosable]="true" [(clrAlertClosed)]="alertClose">
|
||||
<div class="alert-item">
|
||||
<span class="alert-text">
|
||||
{{errorMessage}}
|
||||
</span>
|
||||
</div>
|
||||
</clr-alert>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<span class="spinner spinner-inline" style="top:8px;" [hidden]="showProgress === false"></span>
|
||||
<button type="button" class="btn btn-outline" (click)="close()">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" [disabled]="!isValid || showProgress" (click)="submit()">Ok</button>
|
||||
</div>
|
||||
</clr-modal>
|
|
@ -0,0 +1,103 @@
|
|||
import { Component, OnInit, ViewChild, AfterViewChecked } from '@angular/core';
|
||||
import { NgForm } from '@angular/forms';
|
||||
|
||||
import { SessionUser } from '../../shared/session-user';
|
||||
import { SessionService } from '../../shared/session.service';
|
||||
|
||||
@Component({
|
||||
selector: "account-settings-modal",
|
||||
templateUrl: "account-settings-modal.component.html"
|
||||
})
|
||||
|
||||
export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
|
||||
opened: boolean = false;
|
||||
staticBackdrop: boolean = true;
|
||||
account: SessionUser;
|
||||
error: any;
|
||||
alertClose: boolean = true;
|
||||
|
||||
private isOnCalling: boolean = false;
|
||||
private formValueChanged: boolean = false;
|
||||
|
||||
accountFormRef: NgForm;
|
||||
@ViewChild("accountSettingsFrom") accountForm: NgForm;
|
||||
|
||||
constructor(private session: SessionService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
//Value copy
|
||||
this.account = Object.assign({}, this.session.getCurrentUser());
|
||||
}
|
||||
|
||||
public get isValid(): boolean {
|
||||
return this.accountForm && this.accountForm.valid;
|
||||
}
|
||||
|
||||
public get showProgress(): boolean {
|
||||
return this.isOnCalling;
|
||||
}
|
||||
|
||||
public get errorMessage(): string {
|
||||
if(this.error){
|
||||
if(this.error.message){
|
||||
return this.error.message;
|
||||
}else{
|
||||
if(this.error._body){
|
||||
return this.error._body;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
ngAfterViewChecked(): void {
|
||||
if (this.accountFormRef != this.accountForm) {
|
||||
this.accountFormRef = this.accountForm;
|
||||
if (this.accountFormRef) {
|
||||
this.accountFormRef.valueChanges.subscribe(data => {
|
||||
if (this.error) {
|
||||
this.error = null;
|
||||
}
|
||||
this.formValueChanged = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open() {
|
||||
this.account = Object.assign({}, this.session.getCurrentUser());
|
||||
this.formValueChanged = false;
|
||||
|
||||
this.opened = true;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.opened = false;
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (!this.isValid || this.isOnCalling) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Double confirm session is valid
|
||||
let cUser = this.session.getCurrentUser();
|
||||
if (!cUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isOnCalling = true;
|
||||
|
||||
this.session.updateAccountSettings(this.account)
|
||||
.then(() => {
|
||||
this.isOnCalling = false;
|
||||
this.close();
|
||||
})
|
||||
.catch(error => {
|
||||
this.isOnCalling = false;
|
||||
this.error = error;
|
||||
this.alertClose = false;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { CoreModule } from '../core/core.module';
|
||||
|
||||
import { SignInComponent } from './sign-in/sign-in.component';
|
||||
import { PasswordSettingComponent } from './password/password-setting.component';
|
||||
import { AccountSettingsModalComponent } from './account-settings/account-settings-modal.component';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
|
||||
import { PasswordSettingService } from './password/password-setting.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CoreModule,
|
||||
RouterModule,
|
||||
SharedModule
|
||||
],
|
||||
declarations: [SignInComponent, PasswordSettingComponent, AccountSettingsModalComponent],
|
||||
exports: [SignInComponent, PasswordSettingComponent, AccountSettingsModalComponent],
|
||||
|
||||
providers: [PasswordSettingService]
|
||||
})
|
||||
export class AccountModule { }
|
|
@ -0,0 +1,98 @@
|
|||
import { Component, ViewChild, AfterViewChecked } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgForm } from '@angular/forms';
|
||||
|
||||
import { PasswordSettingService } from './password-setting.service';
|
||||
import { SessionService } from '../../shared/session.service';
|
||||
|
||||
@Component({
|
||||
selector: 'password-setting',
|
||||
templateUrl: "password-setting.component.html"
|
||||
})
|
||||
export class PasswordSettingComponent implements AfterViewChecked {
|
||||
opened: boolean = false;
|
||||
oldPwd: string = "";
|
||||
newPwd: string = "";
|
||||
reNewPwd: string = "";
|
||||
|
||||
private formValueChanged: boolean = false;
|
||||
private onCalling: boolean = false;
|
||||
|
||||
pwdFormRef: NgForm;
|
||||
@ViewChild("changepwdForm") pwdForm: NgForm;
|
||||
constructor(private passwordService: PasswordSettingService, private session: SessionService){}
|
||||
|
||||
//If form is valid
|
||||
public get isValid(): boolean {
|
||||
if (this.pwdForm && this.pwdForm.form.get("newPassword")) {
|
||||
return this.pwdForm.valid &&
|
||||
this.pwdForm.form.get("newPassword").value === this.pwdForm.form.get("reNewPassword").value;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public get valueChanged(): boolean {
|
||||
return this.formValueChanged;
|
||||
}
|
||||
|
||||
public get showProgress(): boolean {
|
||||
return this.onCalling;
|
||||
}
|
||||
|
||||
ngAfterViewChecked() {
|
||||
if (this.pwdFormRef != this.pwdForm) {
|
||||
this.pwdFormRef = this.pwdForm;
|
||||
if (this.pwdFormRef) {
|
||||
this.pwdFormRef.valueChanges.subscribe(data => {
|
||||
this.formValueChanged = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Open modal dialog
|
||||
open(): void {
|
||||
this.opened = true;
|
||||
this.pwdForm.reset();
|
||||
}
|
||||
|
||||
//Close the moal dialog
|
||||
close(): void {
|
||||
this.opened = false;
|
||||
}
|
||||
|
||||
//handle the ok action
|
||||
doOk(): void {
|
||||
if (this.onCalling) {
|
||||
return;//To avoid duplicate click events
|
||||
}
|
||||
|
||||
if (!this.isValid) {
|
||||
return;//Double confirm
|
||||
}
|
||||
|
||||
//Double confirm session is valid
|
||||
let cUser = this.session.getCurrentUser();
|
||||
if(!cUser){
|
||||
return;
|
||||
}
|
||||
|
||||
//Call service
|
||||
this.onCalling = true;
|
||||
|
||||
this.passwordService.changePassword(cUser.user_id,
|
||||
{
|
||||
new_password: this.pwdForm.value.newPassword,
|
||||
old_password: this.pwdForm.value.oldPassword
|
||||
})
|
||||
.then(() => {
|
||||
this.onCalling = false;
|
||||
this.close();
|
||||
})
|
||||
.catch(error => {
|
||||
this.onCalling = false;
|
||||
console.error(error);//TODO:
|
||||
});
|
||||
//TODO:publish the successful message to general messae box
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<div class="login-wrapper">
|
||||
<form #signInForm="ngForm" class="login">
|
||||
<label class="title">
|
||||
VMware Harbor<span class="trademark">™</span>
|
||||
</label>
|
||||
<div class="login-group">
|
||||
<label for="username" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="userNameInput.invalid && (userNameInput.dirty || userNameInput.touched)">
|
||||
<input class="username" type="text" required
|
||||
[(ngModel)]="signInCredential.principal"
|
||||
name="login_username" id="login_username" placeholder="Username"
|
||||
#userNameInput='ngModel'>
|
||||
<span class="tooltip-content">
|
||||
Username is required!
|
||||
</span>
|
||||
</label>
|
||||
<label for="username" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="passwordInput.invalid && (passwordInput.dirty || passwordInput.touched)">
|
||||
<input class="password" type="password" required
|
||||
[(ngModel)]="signInCredential.password"
|
||||
name="login_password" id="login_password" placeholder="Password"
|
||||
#passwordInput="ngModel">
|
||||
<span class="tooltip-content">
|
||||
Password is required!
|
||||
</span>
|
||||
</label>
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" id="rememberme">
|
||||
<label for="rememberme">
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
<div [class.visibility-hidden]="signInStatus != statusError" class="error active">
|
||||
Invalid user name or password
|
||||
</div>
|
||||
<button [disabled]="signInStatus === statusOnGoing" type="submit" class="btn btn-primary" (click)="signIn()">LOG IN</button>
|
||||
<a href="javascript:void(0)" class="signup" (click)="signUp()">Sign up for an account</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,27 @@
|
|||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpModule } from '@angular/http';
|
||||
import { ClarityModule } from 'clarity-angular';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
import { BaseModule } from './base/base.module';
|
||||
import { HarborRoutingModule } from './harbor-routing.module';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
import { AccountModule } from './account/account.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
],
|
||||
imports: [
|
||||
SharedModule,
|
||||
BaseModule,
|
||||
AccountModule,
|
||||
HarborRoutingModule
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { HarborShellComponent } from './harbor-shell/harbor-shell.component';
|
||||
|
||||
import { DashboardComponent } from '../dashboard/dashboard.component';
|
||||
import { ProjectComponent } from '../project/project.component';
|
||||
import { UserComponent } from '../user/user.component';
|
||||
|
||||
import { BaseRoutingResolver } from './base-routing-resolver.service';
|
||||
|
||||
const baseRoutes: Routes = [
|
||||
{
|
||||
path: 'harbor',
|
||||
component: HarborShellComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
component: DashboardComponent
|
||||
},
|
||||
{
|
||||
path: 'projects',
|
||||
component: ProjectComponent
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
component: UserComponent,
|
||||
resolve: {
|
||||
projectsResolver: BaseRoutingResolver
|
||||
}
|
||||
}
|
||||
]
|
||||
}];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(baseRoutes)
|
||||
],
|
||||
exports: [RouterModule],
|
||||
|
||||
providers: [BaseRoutingResolver]
|
||||
})
|
||||
export class BaseRoutingModule {
|
||||
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
|
||||
import { DashboardModule } from '../dashboard/dashboard.module';
|
||||
import { ProjectModule } from '../project/project.module';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { AccountModule } from '../account/account.module';
|
||||
|
||||
import { NavigatorComponent } from './navigator/navigator.component';
|
||||
import { GlobalSearchComponent } from './global-search/global-search.component';
|
||||
import { FooterComponent } from './footer/footer.component';
|
||||
import { HarborShellComponent } from './harbor-shell/harbor-shell.component';
|
||||
import { SearchResultComponent } from './global-search/search-result.component';
|
||||
|
||||
import { BaseRoutingModule } from './base-routing.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
DashboardModule,
|
||||
ProjectModule,
|
||||
UserModule,
|
||||
BaseRoutingModule,
|
||||
AccountModule
|
||||
],
|
||||
declarations: [
|
||||
NavigatorComponent,
|
||||
GlobalSearchComponent,
|
||||
FooterComponent,
|
||||
HarborShellComponent,
|
||||
SearchResultComponent
|
||||
],
|
||||
exports: [ HarborShellComponent ]
|
||||
})
|
||||
export class BaseModule {
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<clr-main-container>
|
||||
<navigator (showAccountSettingsModal)="openModal($event)" (searchEvt)="doSearch($event)" (showPwdChangeModal)="openModal($event)"></navigator>
|
||||
<global-message></global-message>
|
||||
<div class="content-container">
|
||||
<div class="content-area" [class.container-override]="showSearch">
|
||||
<!-- Only appear when searching -->
|
||||
<search-result (closeEvt)="searchClose($event)"></search-result>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<nav class="sidenav" [class.side-nav-override]="showSearch">
|
||||
<section class="sidenav-content">
|
||||
<a routerLink="/harbor/projects" routerLinkActive="active" class="nav-link">
|
||||
Projects
|
||||
</a>
|
||||
<section class="nav-group collapsible">
|
||||
<input id="tabsystem" type="checkbox">
|
||||
<label for="tabsystem">System Managements</label>
|
||||
<ul class="nav-list">
|
||||
<li><a class="nav-link" routerLink="/harbor/users" routerLinkActive="active">Users</a></li>
|
||||
<li><a class="nav-link">Replications</a></li>
|
||||
<li><a class="nav-link">Quarantine[*]</a></li>
|
||||
<li><a class="nav-link">Configurations[*]</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
</nav>
|
||||
</div>
|
||||
</clr-main-container>
|
||||
<account-settings-modal></account-settings-modal>
|
||||
<password-setting></password-setting>
|
|
@ -0,0 +1,80 @@
|
|||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { ModalEvent } from '../modal-event';
|
||||
import { SearchEvent } from '../search-event';
|
||||
import { modalAccountSettings, modalPasswordSetting } from '../modal-events.const';
|
||||
|
||||
import { AccountSettingsModalComponent } from '../../account/account-settings/account-settings-modal.component';
|
||||
import { SearchResultComponent } from '../global-search/search-result.component';
|
||||
import { PasswordSettingComponent } from '../../account/password/password-setting.component';
|
||||
import { NavigatorComponent } from '../navigator/navigator.component';
|
||||
|
||||
@Component({
|
||||
selector: 'harbor-shell',
|
||||
templateUrl: 'harbor-shell.component.html',
|
||||
styleUrls: ["harbor-shell.component.css"]
|
||||
})
|
||||
|
||||
export class HarborShellComponent implements OnInit {
|
||||
|
||||
@ViewChild(AccountSettingsModalComponent)
|
||||
private accountSettingsModal: AccountSettingsModalComponent;
|
||||
|
||||
@ViewChild(SearchResultComponent)
|
||||
private searchResultComponet: SearchResultComponent;
|
||||
|
||||
@ViewChild(PasswordSettingComponent)
|
||||
private pwdSetting: PasswordSettingComponent;
|
||||
|
||||
@ViewChild(NavigatorComponent)
|
||||
private navigator: NavigatorComponent;
|
||||
|
||||
//To indicator whwther or not the search results page is displayed
|
||||
//We need to use this property to do some overriding work
|
||||
private isSearchResultsOpened: boolean = false;
|
||||
|
||||
constructor(private route: ActivatedRoute) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe(data => {
|
||||
//dummy
|
||||
});
|
||||
}
|
||||
|
||||
public get showSearch(): boolean {
|
||||
return this.isSearchResultsOpened;
|
||||
}
|
||||
|
||||
//Open modal dialog
|
||||
openModal(event: ModalEvent): void {
|
||||
switch (event.modalName) {
|
||||
case modalAccountSettings:
|
||||
this.accountSettingsModal.open();
|
||||
break;
|
||||
case modalPasswordSetting:
|
||||
this.pwdSetting.open();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//Handle the global search event and then let the result page to trigger api
|
||||
doSearch(event: SearchEvent): void {
|
||||
//Once this method is called
|
||||
//the search results page must be opened
|
||||
this.isSearchResultsOpened = true;
|
||||
|
||||
//Call the child component to do the real work
|
||||
this.searchResultComponet.doSearch(event.term);
|
||||
}
|
||||
|
||||
//Search results page closed
|
||||
//remove the related ovevriding things
|
||||
searchClose(event: boolean): void {
|
||||
if (event) {
|
||||
this.isSearchResultsOpened = false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
<clr-header class="header-5 header">
|
||||
<div class="branding">
|
||||
<a href="#" class="nav-link">
|
||||
<clr-icon shape="vm-bug"></clr-icon>
|
||||
<span class="title">Harbor</span>
|
||||
</a>
|
||||
</div>
|
||||
<global-search (searchEvt)="transferSearchEvent($event)"></global-search>
|
||||
<div class="header-actions">
|
||||
<div *ngIf="!isSessionValid">
|
||||
<a href="javascript:void(0)" class="nav-link nav-text sign-in-override" routerLink="/sign-in" routerLinkActive="active">Sign In</a>
|
||||
<span class="custom-divider"></span>
|
||||
<a href="javascript:void(0)" class="nav-link nav-text sign-up-override" routerLink="/sign-up" routerLinkActive="active">Sign Up</a>
|
||||
</div>
|
||||
<clr-dropdown class="dropdown bottom-left">
|
||||
<button class="nav-icon" clrDropdownToggle style="width: 90px;">
|
||||
<clr-icon shape="world" style="left:-5px;"></clr-icon>
|
||||
<span>English</span>
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a href="javascript:void(0)" clrDropdownItem>English</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem>中文简体</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem>中文繁體</a>
|
||||
</div>
|
||||
</clr-dropdown>
|
||||
<clr-dropdown [clrMenuPosition]="'bottom-right'" class="dropdown" *ngIf="isSessionValid">
|
||||
<button class="nav-text" clrDropdownToggle>
|
||||
<clr-icon shape="user" class="is-inverse" size="24" style="left: -2px;"></clr-icon>
|
||||
<span>{{accountName}}</span>
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="openAccountSettingsModal()">User Profile</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="openChangePwdModal()">Change Password</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem>About</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="logOut()">Log out</a>
|
||||
</div>
|
||||
</clr-dropdown>
|
||||
</div>
|
||||
</clr-header>
|
|
@ -0,0 +1,70 @@
|
|||
import { Component, Output, EventEmitter, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ModalEvent } from '../modal-event';
|
||||
import { SearchEvent } from '../search-event';
|
||||
import { modalAccountSettings, modalPasswordSetting } from '../modal-events.const';
|
||||
|
||||
import { SessionUser } from '../../shared/session-user';
|
||||
import { SessionService } from '../../shared/session.service';
|
||||
|
||||
@Component({
|
||||
selector: 'navigator',
|
||||
templateUrl: "navigator.component.html",
|
||||
styleUrls: ["navigator.component.css"]
|
||||
})
|
||||
|
||||
export class NavigatorComponent implements OnInit {
|
||||
// constructor(private router: Router){}
|
||||
@Output() showAccountSettingsModal = new EventEmitter<ModalEvent>();
|
||||
@Output() searchEvt = new EventEmitter<SearchEvent>();
|
||||
@Output() showPwdChangeModal = new EventEmitter<ModalEvent>();
|
||||
|
||||
private sessionUser: SessionUser = null;
|
||||
|
||||
constructor(private session: SessionService, private router: Router) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.sessionUser = this.session.getCurrentUser();
|
||||
}
|
||||
|
||||
public get isSessionValid(): boolean {
|
||||
return this.sessionUser != null;
|
||||
}
|
||||
|
||||
public get accountName(): string {
|
||||
return this.sessionUser?this.sessionUser.username: "";
|
||||
}
|
||||
|
||||
//Open the account setting dialog
|
||||
openAccountSettingsModal(): void {
|
||||
this.showAccountSettingsModal.emit({
|
||||
modalName: modalAccountSettings,
|
||||
modalFlag: true
|
||||
});
|
||||
}
|
||||
|
||||
//Open change password dialog
|
||||
openChangePwdModal(): void {
|
||||
this.showPwdChangeModal.emit({
|
||||
modalName: modalPasswordSetting,
|
||||
modalFlag: true
|
||||
});
|
||||
}
|
||||
|
||||
//Only transfer the search event to the parent shell
|
||||
transferSearchEvent(evt: SearchEvent): void {
|
||||
this.searchEvt.emit(evt);
|
||||
}
|
||||
|
||||
//Log out system
|
||||
logOut(): void {
|
||||
this.session.signOff()
|
||||
.then(() => {
|
||||
this.sessionUser = null;
|
||||
//Naviagte to the sign in route
|
||||
this.router.navigate(["/sign-in"]);
|
||||
})
|
||||
.catch()//TODO:
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="row flex-items-xs-right">
|
||||
<div class="col-xs-3 push-md-2 flex-xs-middle">
|
||||
<button class="btn btn-link" (click)="toggleOptionalName(currentOption)">{{toggleName[currentOption]}}</button>
|
||||
</div>
|
||||
<div class="col-xs-3 flex-xs-middle">
|
||||
<clr-icon shape="filter" style="position: relative; left: 15px;"></clr-icon><input style="padding-left: 20px;" type="text" placeholder="Filter logs" #searchUsername (keyup.enter)="doSearchAuditLogs(searchUsername.value)">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row flex-items-xs-right advance-option" [hidden]="currentOption === 0">
|
||||
<div class="col-xs-2 push-md-1">
|
||||
<clr-dropdown [clrMenuPosition]="'bottom-left'" >
|
||||
<button class="btn btn-link" clrDropdownToggle>
|
||||
All Operations
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a href="javascript:void(0)" clrDropdownItem *ngFor="let f of filterOptions" (click)="toggleFilterOption(f.key)"><clr-icon shape="check" [hidden]="!f.checked"></clr-icon> {{f.description}}</a>
|
||||
</div>
|
||||
</clr-dropdown>
|
||||
</div>
|
||||
<div class="col-xs-5 push-md-1">
|
||||
<clr-icon shape="date"></clr-icon><input type="date" #fromTime (change)="doSearchByTimeRange(fromTime.value, 'begin')">
|
||||
<clr-icon shape="date"></clr-icon><input type="date" #toTime (change)="doSearchByTimeRange(toTime.value, 'end')">
|
||||
</div>
|
||||
</div>
|
||||
<clr-datagrid>
|
||||
<clr-dg-column>Username</clr-dg-column>
|
||||
<clr-dg-column>Repository Name</clr-dg-column>
|
||||
<clr-dg-column>Tag</clr-dg-column>
|
||||
<clr-dg-column>Operation</clr-dg-column>
|
||||
<clr-dg-column>Timestamp</clr-dg-column>
|
||||
<clr-dg-row *ngFor="let l of auditLogs">
|
||||
<clr-dg-cell>{{l.username}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.repo_name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.repo_tag}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.operation}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.op_time}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>{{ (auditLogs ? auditLogs.length : 0) }} item(s)</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,138 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
|
||||
import { AuditLog } from './audit-log';
|
||||
import { SessionUser } from '../shared/session-user';
|
||||
|
||||
import { AuditLogService } from './audit-log.service';
|
||||
import { SessionService } from '../shared/session.service';
|
||||
import { MessageService } from '../global-message/message.service';
|
||||
|
||||
export const optionalSearch: {} = {0: 'Advanced', 1: 'Simple'};
|
||||
|
||||
|
||||
export class FilterOption {
|
||||
key: string;
|
||||
description: string;
|
||||
checked: boolean;
|
||||
|
||||
constructor(private iKey: string, private iDescription: string, private iChecked: boolean) {
|
||||
this.key = iKey;
|
||||
this.description = iDescription;
|
||||
this.checked = iChecked;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return 'key:' + this.key + ', description:' + this.description + ', checked:' + this.checked + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'audit-log',
|
||||
templateUrl: './audit-log.component.html',
|
||||
styleUrls: [ 'audit-log.css' ]
|
||||
})
|
||||
export class AuditLogComponent implements OnInit {
|
||||
|
||||
currentUser: SessionUser;
|
||||
projectId: number;
|
||||
queryParam: AuditLog = new AuditLog();
|
||||
auditLogs: AuditLog[];
|
||||
|
||||
toggleName = optionalSearch;
|
||||
currentOption: number = 0;
|
||||
filterOptions: FilterOption[] = [
|
||||
new FilterOption('all', 'All Operations', true),
|
||||
new FilterOption('pull', 'Pull', true),
|
||||
new FilterOption('push', 'Push', true),
|
||||
new FilterOption('create', 'Create', true),
|
||||
new FilterOption('delete', 'Delete', true),
|
||||
new FilterOption('others', 'Others', true)
|
||||
];
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router, private auditLogService: AuditLogService, private messageService: MessageService) {
|
||||
//Get current user from registered resolver.
|
||||
this.route.data.subscribe(data=>this.currentUser = <SessionUser>data['auditLogResolver']);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.projectId = +this.route.snapshot.parent.params['id'];
|
||||
console.log('Get projectId from route params snapshot:' + this.projectId);
|
||||
this.queryParam.project_id = this.projectId;
|
||||
this.retrieve(this.queryParam);
|
||||
}
|
||||
|
||||
retrieve(queryParam: AuditLog): void {
|
||||
this.auditLogService
|
||||
.listAuditLogs(queryParam)
|
||||
.subscribe(
|
||||
response=>this.auditLogs = response,
|
||||
error=>{
|
||||
this.router.navigate(['/harbor', 'projects']);
|
||||
this.messageService.announceMessage('Failed to list audit logs with project ID:' + queryParam.project_id);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
doSearchAuditLogs(searchUsername: string): void {
|
||||
this.queryParam.username = searchUsername;
|
||||
this.retrieve(this.queryParam);
|
||||
}
|
||||
|
||||
doSearchByTimeRange(strDate: string, target: string): void {
|
||||
let oneDayOffset = 3600 * 24;
|
||||
switch(target) {
|
||||
case 'begin':
|
||||
this.queryParam.begin_timestamp = new Date(strDate).getTime() / 1000;
|
||||
break;
|
||||
case 'end':
|
||||
this.queryParam.end_timestamp = new Date(strDate).getTime() / 1000 + oneDayOffset;
|
||||
break;
|
||||
}
|
||||
console.log('Search audit log filtered by time range, begin: ' + this.queryParam.begin_timestamp + ', end:' + this.queryParam.end_timestamp);
|
||||
this.retrieve(this.queryParam);
|
||||
}
|
||||
|
||||
doSearchByOptions() {
|
||||
let selectAll = true;
|
||||
let operationFilter: string[] = [];
|
||||
for(var i in this.filterOptions) {
|
||||
let filterOption = this.filterOptions[i];
|
||||
if(filterOption.checked) {
|
||||
operationFilter.push(this.filterOptions[i].key);
|
||||
}else{
|
||||
selectAll = false;
|
||||
}
|
||||
}
|
||||
if(selectAll) {
|
||||
operationFilter = [];
|
||||
}
|
||||
this.queryParam.keywords = operationFilter.join('/');
|
||||
this.retrieve(this.queryParam);
|
||||
console.log('Search option filter:' + operationFilter.join('/'));
|
||||
}
|
||||
|
||||
toggleOptionalName(option: number): void {
|
||||
(option === 1) ? this.currentOption = 0 : this.currentOption = 1;
|
||||
}
|
||||
|
||||
toggleFilterOption(option: string): void {
|
||||
let selectedOption = this.filterOptions.find(value =>(value.key === option));
|
||||
selectedOption.checked = !selectedOption.checked;
|
||||
if(selectedOption.key === 'all') {
|
||||
this.filterOptions.filter(value=> value.key !== selectedOption.key).forEach(value => value.checked = selectedOption.checked);
|
||||
} else {
|
||||
if(!selectedOption.checked) {
|
||||
this.filterOptions.find(value=>value.key === 'all').checked = false;
|
||||
}
|
||||
let selectAll = true;
|
||||
this.filterOptions.filter(value=> value.key !== 'all').forEach(value =>{
|
||||
if(!value.checked) {
|
||||
selectAll = false;
|
||||
}
|
||||
});
|
||||
this.filterOptions.find(value=>value.key === 'all').checked = selectAll;
|
||||
}
|
||||
this.doSearchByOptions();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.advance-option {
|
||||
font-size: 12px;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Http, Headers, RequestOptions } from '@angular/http';
|
||||
|
||||
import { BaseService } from '../service/base.service';
|
||||
|
||||
import { AuditLog } from './audit-log';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/operator/catch';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/observable/throw';
|
||||
|
||||
export const urlPrefix = '';
|
||||
|
||||
@Injectable()
|
||||
export class AuditLogService extends BaseService {
|
||||
|
||||
constructor(private http: Http) {
|
||||
super();
|
||||
}
|
||||
|
||||
listAuditLogs(queryParam: AuditLog): Observable<AuditLog[]> {
|
||||
return this.http
|
||||
.post(urlPrefix + `/api/projects/${queryParam.project_id}/logs/filter`, {
|
||||
begin_timestamp: queryParam.begin_timestamp,
|
||||
end_timestamp: queryParam.end_timestamp,
|
||||
keywords: queryParam.keywords,
|
||||
operation: queryParam.operation,
|
||||
project_id: queryParam.project_id,
|
||||
username: queryParam.username })
|
||||
.map(response=>response.json() as AuditLog[])
|
||||
.catch(error=>this.handleError(error));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
{
|
||||
"log_id": 3,
|
||||
"user_id": 0,
|
||||
"project_id": 0,
|
||||
"repo_name": "library/mysql",
|
||||
"repo_tag": "5.6",
|
||||
"guid": "",
|
||||
"operation": "push",
|
||||
"op_time": "2017-02-14T09:22:58Z",
|
||||
"username": "admin",
|
||||
"keywords": "",
|
||||
"BeginTime": "0001-01-01T00:00:00Z",
|
||||
"begin_timestamp": 0,
|
||||
"EndTime": "0001-01-01T00:00:00Z",
|
||||
"end_timestamp": 0
|
||||
}
|
||||
*/
|
||||
export class AuditLog {
|
||||
log_id: number;
|
||||
project_id: number;
|
||||
username: string;
|
||||
repo_name: string;
|
||||
repo_tag: string;
|
||||
operation: string;
|
||||
op_time: Date;
|
||||
begin_timestamp: number = 0;
|
||||
end_timestamp: number = 0;
|
||||
keywords: string;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { AuditLogComponent } from './audit-log.component';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { AuditLogService } from './audit-log.service';
|
||||
@NgModule({
|
||||
imports: [ SharedModule ],
|
||||
declarations: [ AuditLogComponent ],
|
||||
providers: [ AuditLogService ],
|
||||
exports: [ AuditLogComponent ]
|
||||
})
|
||||
export class LogModule {}
|
|
@ -0,0 +1,59 @@
|
|||
import { Component, EventEmitter, Output } from '@angular/core';
|
||||
import { Response } from '@angular/http';
|
||||
|
||||
import { Project } from '../project';
|
||||
import { ProjectService } from '../project.service';
|
||||
|
||||
import { MessageService } from '../../global-message/message.service';
|
||||
|
||||
@Component({
|
||||
selector: 'create-project',
|
||||
templateUrl: 'create-project.component.html',
|
||||
styleUrls: [ 'create-project.css' ]
|
||||
})
|
||||
export class CreateProjectComponent {
|
||||
|
||||
project: Project = new Project();
|
||||
createProjectOpened: boolean;
|
||||
|
||||
errorMessage: string;
|
||||
hasError: boolean;
|
||||
|
||||
@Output() create = new EventEmitter<boolean>();
|
||||
|
||||
constructor(private projectService: ProjectService, private messageService: MessageService) {}
|
||||
|
||||
onSubmit() {
|
||||
this.hasError = false;
|
||||
this.projectService
|
||||
.createProject(this.project.name, this.project.public ? 1 : 0)
|
||||
.subscribe(
|
||||
status=>{
|
||||
this.create.emit(true);
|
||||
this.createProjectOpened = false;
|
||||
},
|
||||
error=>{
|
||||
this.hasError = true;
|
||||
if (error instanceof Response) {
|
||||
switch(error.status) {
|
||||
case 409:
|
||||
this.errorMessage = 'Project name already exists.';
|
||||
break;
|
||||
case 400:
|
||||
this.errorMessage = 'Project name is illegal.';
|
||||
break;
|
||||
default:
|
||||
this.errorMessage = 'Unknown error for project name.';
|
||||
this.messageService.announceMessage(this.errorMessage);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
newProject() {
|
||||
this.hasError = false;
|
||||
this.project = new Project();
|
||||
this.createProjectOpened = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<clr-datagrid>
|
||||
<clr-dg-column>Name</clr-dg-column>
|
||||
<clr-dg-column>Public/Private</clr-dg-column>
|
||||
<clr-dg-column>Repositories</clr-dg-column>
|
||||
<clr-dg-column>Creation time</clr-dg-column>
|
||||
<clr-dg-column>Description</clr-dg-column>
|
||||
<clr-dg-row *clrDgItems="let p of projects" [clrDgItem]="p">
|
||||
<!--<clr-dg-action-overflow>
|
||||
<button class="action-item" (click)="onEdit(p)">Edit</button>
|
||||
<button class="action-item" (click)="onDelete(p)">Delete</button>
|
||||
</clr-dg-action-overflow>-->
|
||||
<clr-dg-cell><a [routerLink]="['/harbor', 'projects', p.project_id, 'repository']" >{{p.name}}</a></clr-dg-cell>
|
||||
<clr-dg-cell>{{p.public == 1 ? 'Public': 'Private'}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{p.repo_count}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{p.creation_time}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
{{p.description}}
|
||||
<span style="float: right;">
|
||||
<action-project (togglePublic)="toggleProject($event)" (deleteProject)="deleteProject($event)" [project]="p"></action-project>
|
||||
</span>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>{{ (projects ? projects.length : 0) }} item(s)</clr-dg-footer>
|
||||
</clr-datagrid>
|
|
@ -0,0 +1,25 @@
|
|||
import { Component, EventEmitter, Output, Input } from '@angular/core';
|
||||
import { Project } from '../project';
|
||||
import { ProjectService } from '../project.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'list-project',
|
||||
templateUrl: 'list-project.component.html'
|
||||
})
|
||||
export class ListProjectComponent {
|
||||
|
||||
@Input() projects: Project[];
|
||||
|
||||
@Output() toggle = new EventEmitter<Project>();
|
||||
@Output() delete = new EventEmitter<Project>();
|
||||
|
||||
toggleProject(p: Project) {
|
||||
this.toggle.emit(p);
|
||||
}
|
||||
|
||||
deleteProject(p: Project) {
|
||||
this.delete.emit(p);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="row flex-items-xs-between">
|
||||
<div class="col-xs-4 flex-xs-middle">
|
||||
<button class="btn btn-link" (click)="openAddMemberModal()"><clr-icon shape="add"></clr-icon> new user</button>
|
||||
<add-member [projectId]="projectId" (added)="addedMember($event)"></add-member>
|
||||
</div>
|
||||
<div class="col-xs-4 flex-xs-middle">
|
||||
<clr-icon shape="filter" style="position: relative; left: 15px;"></clr-icon><input style="padding-left: 20px;" type="text" placeholder="Search for users" #searchMember (keyup.enter)="doSearch(searchMember.value)">
|
||||
</div>
|
||||
</div>
|
||||
<clr-datagrid>
|
||||
<clr-dg-column>Name</clr-dg-column>
|
||||
<clr-dg-column>Role</clr-dg-column>
|
||||
<clr-dg-column>Action</clr-dg-column>
|
||||
<clr-dg-row *ngFor="let u of members">
|
||||
<clr-dg-cell>{{u.username}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{roleInfo[u.role_id]}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<clr-dropdown [clrMenuPosition]="'bottom-left'" [hidden]="u.user_id === currentUser.user_id">
|
||||
<button class="btn btn-sm btn-link" clrDropdownToggle>
|
||||
Actions
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="changeRole(u.user_id, 1)">Project Admin</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="changeRole(u.user_id, 2)">Developer</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="changeRole(u.user_id, 3)">Guest</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="deleteMember(u.user_id)">Delete Member</a>
|
||||
</div>
|
||||
</clr-dropdown>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>{{ (members ? members.length : 0) }} item(s)</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,93 @@
|
|||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
|
||||
import { SessionUser } from '../../shared/session-user';
|
||||
import { Member } from './member';
|
||||
import { MemberService } from './member.service';
|
||||
|
||||
import { AddMemberComponent } from './add-member/add-member.component';
|
||||
|
||||
import { MessageService } from '../../global-message/message.service';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/operator/switchMap';
|
||||
import 'rxjs/add/operator/catch';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/observable/throw';
|
||||
|
||||
export const roleInfo: {} = { 1: 'ProjectAdmin', 2: 'Developer', 3: 'Guest'};
|
||||
|
||||
@Component({
|
||||
templateUrl: 'member.component.html'
|
||||
})
|
||||
export class MemberComponent implements OnInit {
|
||||
|
||||
currentUser: SessionUser;
|
||||
members: Member[];
|
||||
projectId: number;
|
||||
roleInfo = roleInfo;
|
||||
|
||||
@ViewChild(AddMemberComponent)
|
||||
addMemberComponent: AddMemberComponent;
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router, private memberService: MemberService, private messageService: MessageService) {
|
||||
//Get current user from registered resolver.
|
||||
this.route.data.subscribe(data=>this.currentUser = <SessionUser>data['memberResolver']);
|
||||
}
|
||||
|
||||
retrieve(projectId:number, username: string) {
|
||||
this.memberService
|
||||
.listMembers(projectId, username)
|
||||
.subscribe(
|
||||
response=>this.members = response,
|
||||
error=>{
|
||||
this.router.navigate(['/harbor', 'projects']);
|
||||
this.messageService.announceMessage('Failed to get project member with project ID:' + projectId);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
//Get projectId from route params snapshot.
|
||||
this.projectId = +this.route.snapshot.parent.params['id'];
|
||||
console.log('Get projectId from route params snapshot:' + this.projectId);
|
||||
|
||||
this.retrieve(this.projectId, '');
|
||||
}
|
||||
|
||||
openAddMemberModal() {
|
||||
this.addMemberComponent.openAddMemberModal();
|
||||
}
|
||||
|
||||
addedMember() {
|
||||
this.retrieve(this.projectId, '');
|
||||
}
|
||||
|
||||
changeRole(userId: number, roleId: number) {
|
||||
this.memberService
|
||||
.changeMemberRole(this.projectId, userId, roleId)
|
||||
.subscribe(
|
||||
response=>{
|
||||
console.log('Successful change role with user ' + userId + ' to roleId ' + roleId);
|
||||
this.retrieve(this.projectId, '');
|
||||
},
|
||||
error => this.messageService.announceMessage('Failed to change role with user ' + userId + ' to roleId ' + roleId)
|
||||
);
|
||||
}
|
||||
|
||||
deleteMember(userId: number) {
|
||||
this.memberService
|
||||
.deleteMember(this.projectId, userId)
|
||||
.subscribe(
|
||||
response=>{
|
||||
console.log('Successful change role with user ' + userId);
|
||||
this.retrieve(this.projectId, '');
|
||||
},
|
||||
error => this.messageService.announceMessage('Failed to change role with user ' + userId)
|
||||
);
|
||||
}
|
||||
|
||||
doSearch(searchMember) {
|
||||
this.retrieve(this.projectId, searchMember);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<h1 class="display-in-line">{{currentProject.name}}</h1>
|
||||
<nav class="subnav">
|
||||
<ul class="nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="repository" routerLinkActive="active">Repositories</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="replication" routerLinkActive="active">Replication</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="member" routerLinkActive="active">Users</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="log" routerLinkActive="active">Logs</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<router-outlet></router-outlet>
|
|
@ -0,0 +1,18 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { Project } from '../project';
|
||||
|
||||
@Component({
|
||||
selector: 'project-detail',
|
||||
templateUrl: "project-detail.component.html",
|
||||
styleUrls: [ 'project-detail.css' ]
|
||||
})
|
||||
export class ProjectDetailComponent {
|
||||
|
||||
currentProject: Project;
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router) {
|
||||
this.route.data.subscribe(data=>this.currentProject = <Project>data['projectResolver']);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router';
|
||||
|
||||
import { Project } from './project';
|
||||
import { ProjectService } from './project.service';
|
||||
|
||||
@Injectable()
|
||||
export class ProjectRoutingResolver implements Resolve<Project>{
|
||||
|
||||
constructor(private projectService: ProjectService, private router: Router) {}
|
||||
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Project> {
|
||||
let projectId = route.params['id'];
|
||||
return this.projectService
|
||||
.getProject(projectId)
|
||||
.then(project=> {
|
||||
if(project) {
|
||||
return project;
|
||||
} else {
|
||||
this.router.navigate(['/harbor', 'projects']);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { HarborShellComponent } from '../base/harbor-shell/harbor-shell.component';
|
||||
import { ProjectComponent } from './project.component';
|
||||
import { ProjectDetailComponent } from './project-detail/project-detail.component';
|
||||
|
||||
import { RepositoryComponent } from '../repository/repository.component';
|
||||
import { ReplicationComponent } from '../replication/replication.component';
|
||||
import { MemberComponent } from './member/member.component';
|
||||
import { AuditLogComponent } from '../log/audit-log.component';
|
||||
|
||||
import { BaseRoutingResolver } from '../base/base-routing-resolver.service';
|
||||
import { ProjectRoutingResolver } from './project-routing-resolver.service';
|
||||
|
||||
const projectRoutes: Routes = [
|
||||
{
|
||||
path: 'harbor',
|
||||
component: HarborShellComponent,
|
||||
resolve: {
|
||||
harborResolver: BaseRoutingResolver
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'projects',
|
||||
component: ProjectComponent,
|
||||
resolve: {
|
||||
projectsResolver: BaseRoutingResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'projects/:id',
|
||||
component: ProjectDetailComponent,
|
||||
resolve: {
|
||||
projectResolver: ProjectRoutingResolver
|
||||
},
|
||||
children: [
|
||||
{ path: 'repository', component: RepositoryComponent },
|
||||
{ path: 'replication', component: ReplicationComponent },
|
||||
{
|
||||
path: 'member', component: MemberComponent,
|
||||
resolve: {
|
||||
memberResolver: BaseRoutingResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'log', component: AuditLogComponent,
|
||||
resolve: {
|
||||
auditLogResolver: BaseRoutingResolver
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(projectRoutes)
|
||||
],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class ProjectRoutingModule { }
|
|
@ -0,0 +1,23 @@
|
|||
<h1>Projects</h1>
|
||||
<div class="row flex-items-xs-between">
|
||||
<div class="col-xs-4">
|
||||
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> New Project</button>
|
||||
<create-project (create)="createProject($event)" (openModal)="openModal($event)"></create-project>
|
||||
</div>
|
||||
<div class="col-xs-5">
|
||||
<clr-dropdown [clrMenuPosition]="'bottom-left'">
|
||||
<button class="btn btn-sm btn-link" clrDropdownToggle>
|
||||
{{projectTypes[currentFilteredType]}}
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="doFilterProjects(0)">{{projectTypes[0]}}</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="doFilterProjects(1)">{{projectTypes[1]}}</a>
|
||||
</div>
|
||||
</clr-dropdown>
|
||||
<clr-icon shape="filter" style="position: relative; left: 15px;"></clr-icon><input style="padding-left: 20px;" type="text" placeholder="Search for projects" #searchProject (keyup.enter)="doSearchProjects(searchProject.value)" >
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<list-project [projects]="changedProjects" (toggle)="toggleProject($event)" (delete)="deleteProject($event)"></list-project>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,93 @@
|
|||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { Project } from './project';
|
||||
import { ProjectService } from './project.service';
|
||||
|
||||
import { CreateProjectComponent } from './create-project/create-project.component';
|
||||
|
||||
import { ListProjectComponent } from './list-project/list-project.component';
|
||||
|
||||
import { MessageService } from '../global-message/message.service';
|
||||
|
||||
export const types: {} = { 0: 'My Projects', 1: 'Public Projects'};
|
||||
|
||||
@Component({
|
||||
selector: 'project',
|
||||
templateUrl: 'project.component.html',
|
||||
styleUrls: [ 'project.css' ]
|
||||
})
|
||||
export class ProjectComponent implements OnInit {
|
||||
|
||||
selected = [];
|
||||
changedProjects: Project[];
|
||||
projectTypes = types;
|
||||
|
||||
@ViewChild(CreateProjectComponent)
|
||||
creationProject: CreateProjectComponent;
|
||||
|
||||
@ViewChild(ListProjectComponent)
|
||||
listProject: ListProjectComponent;
|
||||
|
||||
currentFilteredType: number = 0;
|
||||
lastFilteredType: number = 0;
|
||||
|
||||
constructor(private projectService: ProjectService, private messageService: MessageService){}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.retrieve('', this.lastFilteredType);
|
||||
}
|
||||
|
||||
retrieve(name: string, isPublic: number): void {
|
||||
this.projectService
|
||||
.listProjects(name, isPublic)
|
||||
.subscribe(
|
||||
response => this.changedProjects = response,
|
||||
error => this.messageService.announceMessage(error));
|
||||
}
|
||||
|
||||
openModal(): void {
|
||||
this.creationProject.newProject();
|
||||
}
|
||||
|
||||
createProject(created: boolean) {
|
||||
if(created) {
|
||||
this.retrieve('', this.lastFilteredType);
|
||||
}
|
||||
}
|
||||
|
||||
doSearchProjects(projectName: string): void {
|
||||
console.log('Search for project name:' + projectName);
|
||||
this.retrieve(projectName, this.lastFilteredType);
|
||||
}
|
||||
|
||||
doFilterProjects(filteredType: number): void {
|
||||
console.log('Filter projects with type:' + types[filteredType]);
|
||||
this.lastFilteredType = filteredType;
|
||||
this.currentFilteredType = filteredType;
|
||||
this.retrieve('', this.lastFilteredType);
|
||||
}
|
||||
|
||||
toggleProject(p: Project) {
|
||||
this.projectService
|
||||
.toggleProjectPublic(p.project_id, p.public)
|
||||
.subscribe(
|
||||
response=>console.log('Successful toggled project_id:' + p.project_id),
|
||||
error=>this.messageService.announceMessage(error)
|
||||
);
|
||||
}
|
||||
|
||||
deleteProject(p: Project) {
|
||||
this.projectService
|
||||
.deleteProject(p.project_id)
|
||||
.subscribe(
|
||||
response=>{
|
||||
console.log('Successful delete project_id:' + p.project_id);
|
||||
this.retrieve('', this.lastFilteredType);
|
||||
},
|
||||
error=>console.log(error)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { RepositoryModule } from '../repository/repository.module';
|
||||
import { ReplicationModule } from '../replication/replication.module';
|
||||
import { LogModule } from '../log/log.module';
|
||||
|
||||
import { ProjectComponent } from './project.component';
|
||||
import { CreateProjectComponent } from './create-project/create-project.component';
|
||||
import { ActionProjectComponent } from './action-project/action-project.component';
|
||||
import { ListProjectComponent } from './list-project/list-project.component';
|
||||
|
||||
import { ProjectDetailComponent } from './project-detail/project-detail.component';
|
||||
|
||||
import { MemberComponent } from './member/member.component';
|
||||
import { AddMemberComponent } from './member/add-member/add-member.component';
|
||||
|
||||
import { ProjectRoutingModule } from './project-routing.module';
|
||||
|
||||
import { ProjectService } from './project.service';
|
||||
import { MemberService } from './member/member.service';
|
||||
import { ProjectRoutingResolver } from './project-routing-resolver.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
RepositoryModule,
|
||||
ReplicationModule,
|
||||
LogModule,
|
||||
ProjectRoutingModule
|
||||
],
|
||||
declarations: [
|
||||
ProjectComponent,
|
||||
CreateProjectComponent,
|
||||
ActionProjectComponent,
|
||||
ListProjectComponent,
|
||||
ProjectDetailComponent,
|
||||
MemberComponent,
|
||||
AddMemberComponent
|
||||
],
|
||||
exports: [ ProjectComponent ],
|
||||
providers: [ ProjectRoutingResolver, ProjectService, MemberService ]
|
||||
})
|
||||
export class ProjectModule {
|
||||
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Http, Headers, RequestOptions } from '@angular/http';
|
||||
import { Project } from './project';
|
||||
|
||||
import { BaseService } from '../service/base.service';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/operator/catch';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/observable/throw';
|
||||
|
||||
const url_prefix = '';
|
||||
|
||||
@Injectable()
|
||||
export class ProjectService extends BaseService {
|
||||
|
||||
headers = new Headers({'Content-type': 'application/json'});
|
||||
options = new RequestOptions({'headers': this.headers});
|
||||
|
||||
constructor(private http: Http) {
|
||||
super();
|
||||
}
|
||||
|
||||
getProject(projectId: number): Promise<Project> {
|
||||
return this.http
|
||||
.get(url_prefix + `/api/projects/${projectId}`)
|
||||
.toPromise()
|
||||
.then(response=>response.json() as Project)
|
||||
.catch(error=>Observable.throw(error));
|
||||
}
|
||||
|
||||
listProjects(name: string, isPublic: number): Observable<Project[]>{
|
||||
return this.http
|
||||
.get(url_prefix + `/api/projects?project_name=${name}&is_public=${isPublic}`, this.options)
|
||||
.map(response=>response.json())
|
||||
.catch(this.handleError);
|
||||
}
|
||||
|
||||
createProject(name: string, isPublic: number): Observable<any> {
|
||||
return this.http
|
||||
.post(url_prefix + `/api/projects`,
|
||||
JSON.stringify({'project_name': name, 'public': isPublic})
|
||||
, this.options)
|
||||
.map(response=>response.status)
|
||||
.catch(error=>Observable.throw(error));
|
||||
}
|
||||
|
||||
toggleProjectPublic(projectId: number, isPublic: number): Observable<any> {
|
||||
return this.http
|
||||
.put(url_prefix + `/api/projects/${projectId}/publicity`, { 'public': isPublic }, this.options)
|
||||
.map(response=>response.status)
|
||||
.catch(error=>Observable.throw(error));
|
||||
}
|
||||
|
||||
deleteProject(projectId: number): Observable<any> {
|
||||
return this.http
|
||||
.delete(url_prefix + `/api/projects/${projectId}`)
|
||||
.map(response=>response.status)
|
||||
.catch(error=>Observable.throw(error));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
[
|
||||
{
|
||||
"project_id": 1,
|
||||
"owner_id": 1,
|
||||
"name": "library",
|
||||
"creation_time": "2017-02-10T07:57:56Z",
|
||||
"creation_time_str": "",
|
||||
"deleted": 0,
|
||||
"owner_name": "",
|
||||
"public": 1,
|
||||
"Togglable": true,
|
||||
"update_time": "2017-02-10T07:57:56Z",
|
||||
"current_user_role_id": 1,
|
||||
"repo_count": 0
|
||||
}
|
||||
]
|
||||
*/
|
||||
export class Project {
|
||||
project_id: number;
|
||||
owner_id: number;
|
||||
name: string;
|
||||
creation_time: Date;
|
||||
creation_time_str: string;
|
||||
deleted: number;
|
||||
owner_name: string;
|
||||
public: number;
|
||||
Togglable: boolean;
|
||||
update_time: Date;
|
||||
current_user_role_id: number;
|
||||
repo_count: number;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue