Initial commit
This commit is contained in:
commit
b780e148cc
|
@ -0,0 +1,2 @@
|
|||
node_modules/
|
||||
dist/
|
|
@ -0,0 +1,21 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw*
|
|
@ -0,0 +1,15 @@
|
|||
FROM node:lts-alpine AS web_build
|
||||
|
||||
WORKDIR /agola-web
|
||||
|
||||
# copy both 'package.json' and 'package-lock.json' (if available)
|
||||
COPY package*.json ./
|
||||
|
||||
# install project dependencies
|
||||
RUN npm install
|
||||
|
||||
# Copy all the source
|
||||
COPY . .
|
||||
|
||||
# Build app
|
||||
RUN npm run build
|
|
@ -0,0 +1,202 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# agola-web
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Run your tests
|
||||
```
|
||||
npm run test
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
]
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"name": "agola-web",
|
||||
"version": "0.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^3.3.92",
|
||||
"ansi_up": "^3.0.0",
|
||||
"axios": "^0.18.0",
|
||||
"bulma": "^0.7.4",
|
||||
"moment": "^2.23.0",
|
||||
"moment-duration-format": "^2.2.2",
|
||||
"vue": "^2.5.21",
|
||||
"vue-router": "^3.0.2",
|
||||
"vue2-filters": "^0.4.1",
|
||||
"vuex": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.4.0",
|
||||
"@vue/cli-plugin-eslint": "^3.4.0",
|
||||
"@vue/cli-service": "^3.4.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"eslint": "^5.12.0",
|
||||
"eslint-plugin-vue": "^5.1.0",
|
||||
"node-sass": "^4.11.0",
|
||||
"sass-loader": "^7.1.0",
|
||||
"vue-template-compiler": "^2.5.21"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"rules": {},
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
"autoprefixer": {}
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie <= 8"
|
||||
]
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
|
||||
<title>agola</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but agola doesn't work properly without JavaScript
|
||||
enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<script src="/config.js" />
|
||||
</script>
|
||||
<script>
|
||||
// default config if no config.js is provided
|
||||
if (!window.CONFIG) {
|
||||
const CONFIG = {
|
||||
API_URL: window.location.protocol + "//" + window.location.hostname + ":8000",
|
||||
API_BASE_PATH: "/api/v1alpha"
|
||||
}
|
||||
window.CONFIG = CONFIG
|
||||
}
|
||||
</script>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,80 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<nav class="navbar is-light has-shadow" role="navigation" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/">
|
||||
<h1>Agola</h1>
|
||||
</a>
|
||||
|
||||
<a
|
||||
role="button"
|
||||
class="navbar-burger burger"
|
||||
aria-label="menu"
|
||||
aria-expanded="false"
|
||||
data-target="navbarBasicExample"
|
||||
>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="navbar-menu">
|
||||
<div class="navbar-start"></div>
|
||||
<div class="navbar-end">
|
||||
<div v-if="user" class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">{{user.username}}</a>
|
||||
<div class="navbar-dropdown">
|
||||
<div class="navbar-item">
|
||||
Logged as
|
||||
<b>{{user.username}}</b>
|
||||
</div>
|
||||
<hr class="navbar-divider">
|
||||
<router-link class="navbar-item" to="/logout">
|
||||
<i class="mdi mdi-logout"></i>Logout
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="navbar-item">
|
||||
<router-link class="button" to="/login">Login</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="main-container container">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
components: {},
|
||||
computed: {
|
||||
...mapGetters(["user"])
|
||||
},
|
||||
watch: {
|
||||
user: function(user) {
|
||||
if (user) {
|
||||
this.$router.push({
|
||||
name: "user",
|
||||
params: { username: this.user.username }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/css/main.scss";
|
||||
|
||||
.main-container {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,156 @@
|
|||
<template>
|
||||
<div class="item">
|
||||
<div
|
||||
class="touchable"
|
||||
:class="stepClass(step)"
|
||||
role="tab"
|
||||
:aria-expanded="active ? 'true' : 'false'"
|
||||
@click.prevent="toggle"
|
||||
>
|
||||
<div class="item-content">
|
||||
<div class="header">
|
||||
<span class="icon">
|
||||
<i
|
||||
class="mdi mdi-arrow-right"
|
||||
:class="{ 'arrow-down': active, 'arrow-right': !active }"
|
||||
></i>
|
||||
</span>
|
||||
<span class="name">{{step.name}}</span>
|
||||
<span class="duration">{{duration}}</span>
|
||||
</div>
|
||||
<div class="log-container" v-show="active">
|
||||
<Log
|
||||
v-bind:runid="runid"
|
||||
v-bind:taskid="taskid"
|
||||
v-bind:step="stepnum"
|
||||
v-bind:stepphase="step.phase"
|
||||
v-bind:show="active"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as moment from "moment";
|
||||
import momentDurationFormatSetup from "moment-duration-format";
|
||||
import Log from "@/components/log.vue";
|
||||
|
||||
momentDurationFormatSetup(moment);
|
||||
|
||||
export default {
|
||||
name: "Collapse",
|
||||
components: {
|
||||
Log
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
active: false,
|
||||
duration: null
|
||||
};
|
||||
},
|
||||
props: {
|
||||
runid: String,
|
||||
taskid: String,
|
||||
stepnum: Number,
|
||||
step: Object
|
||||
},
|
||||
created() {
|
||||
this.updateDuration(this.step);
|
||||
},
|
||||
ready() {
|
||||
if (this.active) {
|
||||
this.$emit("collapse-open", this.index);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
step: function(step) {
|
||||
this.updateDuration(step);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
stepClass(step) {
|
||||
if (step.phase == "success") return "success";
|
||||
if (step.phase == "failed") return "failed";
|
||||
if (step.phase == "stopped") return "failed";
|
||||
if (step.phase == "running") return "running";
|
||||
return "unknown";
|
||||
},
|
||||
updateDuration(step) {
|
||||
let start = moment(step.start_time);
|
||||
let end = moment(step.end_time);
|
||||
if (start === null || end === null) {
|
||||
this.duration = null;
|
||||
return;
|
||||
}
|
||||
this.duration = moment.duration(end.diff(start)).format("h:mm:ss[s]");
|
||||
},
|
||||
toggle() {
|
||||
this.active = !this.active;
|
||||
if (this.active) {
|
||||
this.$emit("collapse-open", this.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/css/_variables.scss";
|
||||
|
||||
.item {
|
||||
}
|
||||
|
||||
.item-content {
|
||||
margin-bottom: 5px;
|
||||
border: 1px solid $grey-lighter;
|
||||
border-left: 0 solid;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.success {
|
||||
border-left: 5px solid $green;
|
||||
}
|
||||
|
||||
.failed {
|
||||
border-left: 5px solid $red;
|
||||
}
|
||||
|
||||
.running {
|
||||
border-left: 5px solid $blue;
|
||||
}
|
||||
|
||||
.unknown {
|
||||
border-left: 5px solid $grey-lighter;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex: 0 0 30%;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.duration {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.arrow-right {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.arrow-down {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,146 @@
|
|||
<template>
|
||||
<div class="dark">
|
||||
<div class="log">
|
||||
<div class="stream-line" v-for="(item, index) in items" :key="index">
|
||||
<div v-html="item"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { apiurl, apiurlwithtoken, fetch } from "@/util/auth";
|
||||
import AnsiUp from "ansi_up";
|
||||
|
||||
export default {
|
||||
name: "Log",
|
||||
props: {
|
||||
show: Boolean,
|
||||
runid: String,
|
||||
taskid: String,
|
||||
step: Number,
|
||||
stepphase: String
|
||||
},
|
||||
computed: {},
|
||||
data() {
|
||||
let formatter = new AnsiUp();
|
||||
formatter.use_classes = true;
|
||||
|
||||
return {
|
||||
items: [],
|
||||
lines: [],
|
||||
formatter: formatter,
|
||||
es: null,
|
||||
fetching: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
fetch: function() {
|
||||
if (this.fetching) {
|
||||
return;
|
||||
}
|
||||
this.fetching = true;
|
||||
if (this.stepphase == "running") {
|
||||
this.streamLogs();
|
||||
}
|
||||
|
||||
if (this.stepphase == "success" || this.stepphase == "failed") {
|
||||
this.getLogs();
|
||||
}
|
||||
},
|
||||
streamLogs: function() {
|
||||
this.es = new EventSource(
|
||||
apiurlwithtoken(
|
||||
"/logs?runID=" +
|
||||
this.runid +
|
||||
"&taskID=" +
|
||||
this.taskid +
|
||||
"&step=" +
|
||||
this.step +
|
||||
"&follow"
|
||||
)
|
||||
);
|
||||
this.es.onmessage = event => {
|
||||
var data = event.data;
|
||||
// TODO(sgotti) ansi_up doesn't handle carriage return (\r), find a way to also handle it
|
||||
this.items.push(this.formatter.ansi_to_html(data));
|
||||
};
|
||||
// don't reconnect on error
|
||||
this.es.onerror = () => {
|
||||
this.es.close();
|
||||
};
|
||||
},
|
||||
getLogs: function() {
|
||||
fetch(
|
||||
apiurl(
|
||||
"/logs?runID=" +
|
||||
this.runid +
|
||||
"&taskID=" +
|
||||
this.taskid +
|
||||
"&step=" +
|
||||
this.step
|
||||
)
|
||||
)
|
||||
.then(r => {
|
||||
if (r.status == 200) {
|
||||
return r.text();
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.then(data => {
|
||||
this.items.push(this.formatter.ansi_to_html(data));
|
||||
});
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show: function(post, pre) {
|
||||
if (pre == false && post == true) {
|
||||
this.fetch();
|
||||
}
|
||||
},
|
||||
stepphase: function(post, pre) {
|
||||
if (pre == "notstarted" && post == "running") {
|
||||
this.streamLogs();
|
||||
}
|
||||
if (pre == "notstarted" && (post == "success" || post == "failed")) {
|
||||
this.getLogs();
|
||||
}
|
||||
|
||||
if (pre == "running" && (post == "success" || post == "failed")) {
|
||||
// TODO(sgotti)
|
||||
}
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
if (this.show) {
|
||||
this.fetch();
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.es !== null) {
|
||||
this.es.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.log {
|
||||
background-color: #222;
|
||||
color: #f1f1f1;
|
||||
font-family: Cousine, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 19px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
padding: 5px;
|
||||
|
||||
.stream-line {
|
||||
pre {
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,52 @@
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<div class="field">
|
||||
<p class="control has-icons-left has-icons-right">
|
||||
<input v-model="username" class="input" type="email" placeholder="Email">
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</span>
|
||||
<span class="icon is-small is-right">
|
||||
<i class="fas fa-check"></i>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<p class="control has-icons-left">
|
||||
<input v-model="password" class="input" type="password" placeholder="Password">
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fas fa-lock"></i>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<p class="control">
|
||||
<button
|
||||
@click="$emit('login', { username, password })"
|
||||
class="button is-info is-fullwidth"
|
||||
>Login with {{name}}</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { apiurl, loginurl, fetch } from "@/util/auth";
|
||||
|
||||
export default {
|
||||
name: "Loginform",
|
||||
props: {
|
||||
name: String
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
username: null,
|
||||
password: null
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<nav class="breadcrumb is-large" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="ownerLink(ownertype, ownername)">{{ownername}}</router-link>
|
||||
</li>
|
||||
<li v-if="projectname">
|
||||
<router-link :to="projectLink(ownertype, ownername, projectname)">{{projectname}}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { ownerLink, projectLink } from "@/util/link.js";
|
||||
|
||||
export default {
|
||||
name: "projbreadcrumbs",
|
||||
components: {},
|
||||
props: {
|
||||
ownertype: String,
|
||||
ownername: String,
|
||||
projectname: String
|
||||
},
|
||||
methods: {
|
||||
ownerLink: ownerLink,
|
||||
projectLink: projectLink
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/css/_variables.scss";
|
||||
</style>
|
|
@ -0,0 +1,80 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="item-list" v-for="project in projects" v-bind:key="project.id">
|
||||
<router-link tag="div" class="item" :to="projectURL(project)">
|
||||
<span class="name">{{project.name}}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { apiurl, fetch } from "@/util/auth";
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
name: "Projects",
|
||||
props: {
|
||||
ownertype: String,
|
||||
ownername: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
projects: [],
|
||||
polling: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
projectURL(project) {
|
||||
if (this.ownertype == "user") {
|
||||
return {
|
||||
name: "user project",
|
||||
params: { username: this.ownername, projectname: project.name }
|
||||
};
|
||||
} else if (this.ownertype == "org") {
|
||||
return {
|
||||
name: "org project",
|
||||
params: { orgname: this.ownername, projectname: project.name }
|
||||
};
|
||||
}
|
||||
},
|
||||
fetchProjects(ownertype, ownername) {
|
||||
let path = "/" + ownertype;
|
||||
if (ownername) {
|
||||
path += "/" + ownername;
|
||||
}
|
||||
path += "/projects";
|
||||
fetch(apiurl(path))
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
console.log(res);
|
||||
let projects = res.projects.map(function(project) {
|
||||
return project;
|
||||
});
|
||||
this.projects = projects;
|
||||
console.log("projects", this.projects);
|
||||
});
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.fetchProjects(this.ownertype, this.ownername);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/css/_variables.scss";
|
||||
|
||||
.item-list {
|
||||
.item {
|
||||
margin-bottom: 5px;
|
||||
border: 1px solid $grey-lighter;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
}
|
||||
.name {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,158 @@
|
|||
<template>
|
||||
<div>
|
||||
<RunDetail :run="run"/>
|
||||
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
<li>
|
||||
<a>Tasks</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="run" class="tasks-list">
|
||||
<div v-for="task in run.sortedTasks" v-bind:key="task.id" :class="taskClass(task)">
|
||||
<div class="task-content">
|
||||
<div class="columns">
|
||||
<router-link class="column is-10" tag="a" :to="runTaskLink(task)">
|
||||
<span class="name">{{task.name}}</span>
|
||||
</router-link>
|
||||
<div class="parents column">
|
||||
<span v-if="parents(task).length > 0">depends on: </span>
|
||||
<span class="parent" v-for="dep in parents(task)" v-bind:key="dep">{{dep}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <span
|
||||
class="duration"
|
||||
v-if="duration && (step.Phase == 'success' || step.Phase == 'failed') "
|
||||
>{{duration}}</span>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { fetchRun } from "@/util/data.js";
|
||||
import { userLocalRunTaskLink, projectRunTaskLink } from "@/util/link.js";
|
||||
|
||||
import RunDetail from "@/components/rundetail.vue";
|
||||
|
||||
export default {
|
||||
name: "run",
|
||||
components: { RunDetail },
|
||||
props: {
|
||||
ownertype: String,
|
||||
ownername: String,
|
||||
projectname: String,
|
||||
runid: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
run: null,
|
||||
polling: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
runTaskLink(task) {
|
||||
if (this.projectname) {
|
||||
return projectRunTaskLink(
|
||||
this.ownertype,
|
||||
this.ownername,
|
||||
this.projectname,
|
||||
this.runid,
|
||||
task.id
|
||||
);
|
||||
} else {
|
||||
return userLocalRunTaskLink(this.ownername, this.runid, task.id);
|
||||
}
|
||||
},
|
||||
parents(task) {
|
||||
return task.depends.map(d => {
|
||||
console.log(d.task_id);
|
||||
return this.run.tasks[d.task_id].name;
|
||||
});
|
||||
},
|
||||
taskClass(task) {
|
||||
if (task.status == "success") return "success";
|
||||
if (task.status == "failed") return "failed";
|
||||
if (task.status == "stopped") return "failed";
|
||||
if (task.status == "running") return "running";
|
||||
return "unknown";
|
||||
},
|
||||
async fetchRun() {
|
||||
this.run = await fetchRun(this.runid);
|
||||
// sort tasks by level
|
||||
let tasks = this.run.tasks;
|
||||
let sortedTasks = Object.keys(this.run.tasks)
|
||||
.sort((a, b) =>
|
||||
tasks[a].level > tasks[b].level
|
||||
? 1
|
||||
: tasks[b].level > tasks[a].level
|
||||
? -1
|
||||
: 0
|
||||
)
|
||||
.map(k => this.run.tasks[k]);
|
||||
this.run.sortedTasks = sortedTasks;
|
||||
console.log("run: ", this.run);
|
||||
},
|
||||
pollData() {
|
||||
this.polling = setInterval(() => {
|
||||
this.fetchRun();
|
||||
}, 2000);
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.fetchRun();
|
||||
this.pollData();
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.polling);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/css/_variables.scss";
|
||||
|
||||
.tasks-list {
|
||||
.task-content {
|
||||
margin-bottom: 5px;
|
||||
border: 1px solid $grey-lighter;
|
||||
border-left: 0 solid;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.success {
|
||||
border-left: 5px solid $green;
|
||||
}
|
||||
|
||||
.failed {
|
||||
border-left: 5px solid $red;
|
||||
}
|
||||
|
||||
.running {
|
||||
border-left: 5px solid $blue;
|
||||
}
|
||||
|
||||
.unknown {
|
||||
border-left: 5px solid $grey-lighter;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.parents {
|
||||
margin-left: 1rem;
|
||||
margin-right: 0rem;
|
||||
font-weight: lighter;
|
||||
font-size: 0.8rem;
|
||||
.parent {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,252 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="run != null">
|
||||
<div class="run">
|
||||
<div :class="runResultClass(run)">
|
||||
<div class="run-content">
|
||||
<div class="item-content columns">
|
||||
<div class="run-title column is-10">
|
||||
<span class="run-name">{{run.name}}</span>
|
||||
<span
|
||||
class="tag"
|
||||
:class="'is-'+runResultClass(run)"
|
||||
>{{ runStatus(run) | capitalize }}</span>
|
||||
<span v-if="stillRunning(run)" class="stillrunning tag">Still running</span>
|
||||
<span v-if="!stillRunning(run)" class="stillrunning"></span>
|
||||
</div>
|
||||
<div class="run-actions column is-2 is-pulled-right">
|
||||
<div class="dropdown is-hoverable is-right" v-if="run.phase == 'finished'">
|
||||
<div class="dropdown-trigger">
|
||||
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
|
||||
<span>Restart</span>
|
||||
<span class="icon is-small">
|
||||
<i class="mdi mdi-restart" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="dropdown-menu" role="menu">
|
||||
<div class="dropdown-content">
|
||||
<a class="dropdown-item" @click="restartRun(run, true)">From start</a>
|
||||
<a class="dropdown-item" @click="restartRun(run)">From failed tasks</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="button is-danger"
|
||||
v-if="run.phase == 'running'"
|
||||
@click="stopRun(run)"
|
||||
>Stop</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-content columns">
|
||||
<div class="commitmessage column">{{run.annotations.message}}</div>
|
||||
<div class="source-info column">
|
||||
<a :href="run.annotations.commit_link" class="commit" target="_blank">
|
||||
<i class="mdi mdi-source-commit mdi-rotate-90"></i>
|
||||
<span>{{run.annotations.commit_sha.substring(0,8)}}</span>
|
||||
</a>
|
||||
<a
|
||||
v-if="run.annotations.event_type == 'push'"
|
||||
:href="run.annotations.branch_link"
|
||||
class="commit"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="mdi mdi-source-branch"></i>
|
||||
<span>{{run.annotations.branch}}</span>
|
||||
</a>
|
||||
<a
|
||||
v-else-if="run.annotations.event_type == 'tag'"
|
||||
:href="run.annotations.tag_link"
|
||||
class="commit"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="mdi mdi-tag"></i>
|
||||
<span>{{run.annotations.tag}}</span>
|
||||
</a>
|
||||
<a
|
||||
v-else-if="run.annotations.event_type == 'pull_request'"
|
||||
:href="run.annotations.pull_request_link"
|
||||
class="commit"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="mdi mdi-source-pull"></i>
|
||||
<span>PR #{{run.annotations.pull_request_id}}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { apiurl, fetch } from "@/util/auth";
|
||||
import { userLocalRunTaskLink, projectRunTaskLink } from "@/util/link.js";
|
||||
|
||||
export default {
|
||||
name: "RunDetail",
|
||||
props: {
|
||||
run: Object
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
methods: {
|
||||
stillRunning(run) {
|
||||
return run.result != "unknown" && run.phase == "running";
|
||||
},
|
||||
runStatus(run) {
|
||||
if (run.phase != "finished") return run.phase;
|
||||
if (run.result != "unknown") return run.result;
|
||||
if (run.stopping) return "stopping";
|
||||
|
||||
return run.result;
|
||||
},
|
||||
runResultClass(run) {
|
||||
status = this.runStatus(run);
|
||||
|
||||
if (status == "queued") return "unknown";
|
||||
if (status == "cancelled") return "failed";
|
||||
if (status == "running") return "running";
|
||||
if (status == "stopping") return "failed";
|
||||
if (status == "stopped") return "failed";
|
||||
if (status == "success") return "success";
|
||||
if (status == "failed") return "failed";
|
||||
return "unknown";
|
||||
},
|
||||
taskClass(task) {
|
||||
if (task.status == "success") return "success";
|
||||
if (task.status == "failed") return "failed";
|
||||
if (task.status == "stopped") return "failed";
|
||||
if (task.status == "running") return "running";
|
||||
return "unknown";
|
||||
},
|
||||
restartRun(run, fromStart) {
|
||||
fetch(apiurl("/run/" + run.id + "/actions"), {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
action_type: "restart",
|
||||
from_start: fromStart
|
||||
})
|
||||
}).then(r => {
|
||||
console.log("r: " + r);
|
||||
if (r.status == 200) {
|
||||
return r.json();
|
||||
}
|
||||
throw Error(r.statusText);
|
||||
});
|
||||
},
|
||||
stopRun(run) {
|
||||
fetch(apiurl("/run/" + run.id + "/actions"), {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
action_type: "stop"
|
||||
})
|
||||
}).then(r => {
|
||||
console.log("r: " + r);
|
||||
if (r.status == 200) {
|
||||
return r.json();
|
||||
}
|
||||
throw Error(r.statusText);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/css/_variables.scss";
|
||||
|
||||
.run {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
.run-content {
|
||||
margin-bottom: 5px;
|
||||
border: 1px solid $grey-lighter;
|
||||
border-left: 0 solid;
|
||||
display: block;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.run-title {
|
||||
align-items: center;
|
||||
padding-left: 5px;
|
||||
margin-bottom: 25px;
|
||||
|
||||
.run-name {
|
||||
padding-left: 5px;
|
||||
font-size: 1.5rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.success {
|
||||
border-left: 0px solid $green;
|
||||
}
|
||||
|
||||
.failed {
|
||||
border-left: 5px solid $red;
|
||||
}
|
||||
|
||||
.running {
|
||||
border-left: 5px solid $blue;
|
||||
}
|
||||
|
||||
.unknown {
|
||||
border-left: 5px solid $grey-lighter;
|
||||
}
|
||||
}
|
||||
|
||||
.run-actions {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
}
|
||||
|
||||
.success {
|
||||
border-left: 5px solid $green;
|
||||
}
|
||||
|
||||
.failed {
|
||||
border-left: 5px solid $red;
|
||||
}
|
||||
|
||||
.running {
|
||||
border-left: 5px solid $blue;
|
||||
}
|
||||
|
||||
.unknown {
|
||||
border-left: 5px solid $grey-lighter;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.commitmessage {
|
||||
}
|
||||
|
||||
.stillrunning {
|
||||
}
|
||||
|
||||
.source-info {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
a {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.commit {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,253 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="item-list">
|
||||
<div class="item" v-for="run in runs" v-bind:key="run.id" :class="runResultClass(run)">
|
||||
<div class="item-content">
|
||||
<router-link
|
||||
v-if="username"
|
||||
tag="div"
|
||||
class="name"
|
||||
:to="userLocalRunLink(username, run.id)"
|
||||
>
|
||||
<span>{{run.name}}</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-else
|
||||
tag="div"
|
||||
class="name"
|
||||
:to="projectRunLink(ownertype, ownername, projectname, run.id)"
|
||||
>
|
||||
<span>{{run.name}}</span>
|
||||
</router-link>
|
||||
<div class="commitmessage">{{run.annotations.message}}</div>
|
||||
<span v-if="stillRunning(run)" class="stillrunning tag">Still running</span>
|
||||
<span v-if="!stillRunning(run)" class="stillrunning"></span>
|
||||
<div class="source-info">
|
||||
<a :href="run.annotations.commit_link" class="commit" target="_blank">
|
||||
<i class="mdi mdi-source-commit mdi-rotate-90"></i>
|
||||
<span>{{run.annotations.commit_sha.substring(0,8)}}</span>
|
||||
</a>
|
||||
<a
|
||||
v-if="run.annotations.event_type == 'push'"
|
||||
:href="run.annotations.branch_link"
|
||||
class="commit"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="mdi mdi-source-branch"></i>
|
||||
<span>{{run.annotations.branch}}</span>
|
||||
</a>
|
||||
<a
|
||||
v-else-if="run.annotations.event_type == 'tag'"
|
||||
:href="run.annotations.tag_link"
|
||||
class="commit"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="mdi mdi-tag"></i>
|
||||
<span>{{run.annotations.tag}}</span>
|
||||
</a>
|
||||
<a
|
||||
v-else-if="run.annotations.event_type == 'pull_request'"
|
||||
:href="run.annotations.pull_request_link"
|
||||
class="commit"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="mdi mdi-source-pull"></i>
|
||||
<span>PR #{{run.annotations.pull_request_id}}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { apiurl, fetch } from "@/util/auth";
|
||||
import { userLocalRunLink, projectRunLink } from "@/util/link.js";
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
name: "runs",
|
||||
props: {
|
||||
ownertype: String,
|
||||
ownername: String,
|
||||
username: String,
|
||||
projectname: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
runs: [],
|
||||
polling: null,
|
||||
project: null,
|
||||
user: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
projectRunLink: projectRunLink,
|
||||
userLocalRunLink: userLocalRunLink,
|
||||
stillRunning(run) {
|
||||
return run.result != "unknown" && run.phase == "running";
|
||||
},
|
||||
runResultClass(run) {
|
||||
if (run.result == "unknown") {
|
||||
if (run.phase == "queued") return "unknown";
|
||||
if (run.phase == "cancelled") return "failed";
|
||||
if (run.phase == "running") return "running";
|
||||
}
|
||||
if (run.result == "success") return "success";
|
||||
if (run.result == "failed") return "failed";
|
||||
if (run.result == "stopped") return "failed";
|
||||
return "unknown";
|
||||
},
|
||||
fetchProjectRuns() {
|
||||
fetch(
|
||||
apiurl(
|
||||
"/projects/" +
|
||||
this.ownertype +
|
||||
"/" +
|
||||
this.ownername +
|
||||
"/" +
|
||||
this.projectname
|
||||
)
|
||||
)
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
console.log(res);
|
||||
this.project = res;
|
||||
console.log("project", this.project);
|
||||
|
||||
this.fetchRuns();
|
||||
});
|
||||
},
|
||||
fetchUserRuns() {
|
||||
fetch(apiurl("/users/" + this.username))
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
console.log(res);
|
||||
this.user = res;
|
||||
console.log("user", this.user);
|
||||
|
||||
this.fetchRuns();
|
||||
});
|
||||
},
|
||||
fetchRuns() {
|
||||
let u = apiurl("/runs");
|
||||
//console.log("this.project.id", this.project.id);
|
||||
console.log("u", u);
|
||||
if (this.project !== null) {
|
||||
u.searchParams.append("group", this.project.id);
|
||||
} else if (this.user !== null) {
|
||||
u.searchParams.append("group", this.user.id);
|
||||
}
|
||||
|
||||
fetch(u)
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
console.log(res);
|
||||
let runs = res.runs.map(function(run) {
|
||||
return run;
|
||||
});
|
||||
this.runs = runs;
|
||||
console.log("runs", this.runs);
|
||||
});
|
||||
},
|
||||
pollData() {
|
||||
this.polling = setInterval(() => {
|
||||
this.fetchRuns();
|
||||
}, 2000);
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
console.log("username", this.username);
|
||||
console.log("projectname", this.projectname);
|
||||
if (this.projectname !== undefined) {
|
||||
this.fetchProjectRuns();
|
||||
} else if (this.username !== undefined) {
|
||||
this.fetchUserRuns();
|
||||
} else {
|
||||
this.fetchRuns();
|
||||
}
|
||||
this.pollData();
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.polling);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/css/_variables.scss";
|
||||
|
||||
.project-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 5px;
|
||||
margin-bottom: 25px;
|
||||
.project-name {
|
||||
padding-left: 5px;
|
||||
font-size: 1.5rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.item-list {
|
||||
.item {
|
||||
}
|
||||
|
||||
.item-content {
|
||||
margin-bottom: 5px;
|
||||
border: 1px solid $grey-lighter;
|
||||
border-left: 0 solid;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.success {
|
||||
border-left: 5px solid $green;
|
||||
}
|
||||
|
||||
.failed {
|
||||
border-left: 5px solid $red;
|
||||
}
|
||||
|
||||
.running {
|
||||
border-left: 5px solid $blue;
|
||||
}
|
||||
|
||||
.unknown {
|
||||
border-left: 5px solid $grey-lighter;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex: 0 0 30%;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.commitmessage {
|
||||
flex: 0 0 40%;
|
||||
}
|
||||
|
||||
.stillrunning {
|
||||
flex: 0 0 10%;
|
||||
}
|
||||
|
||||
.source-info {
|
||||
flex: 0 0 10%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
a {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.commit {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<div class="arrow">
|
||||
<svg viewBox="0 0 15 15">
|
||||
<path fill="none" stroke="#9d9d9d" d="M4.32.5l6.247 6.942L4.32 14.5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "tabarrow"
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.arrow {
|
||||
width: 1.2em;
|
||||
height: 1.4em;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
top: 0.1rem;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,88 @@
|
|||
<template>
|
||||
<div>
|
||||
<RunDetail :run="run"/>
|
||||
<div v-if="task != null">
|
||||
<div class="task-title">
|
||||
<span class="task-name" v-html="task.name"/>
|
||||
<span class="tag" :class="taskClass(task)">{{ task.status | capitalize }}</span>
|
||||
</div>
|
||||
<div v-for="(step, index) in task.steps" v-bind:key="index">
|
||||
<Collapse
|
||||
v-bind:runid="runid"
|
||||
v-bind:taskid="taskid"
|
||||
v-bind:stepnum="index"
|
||||
v-bind:step="step"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { apiurl, fetch } from "@/util/auth";
|
||||
import { fetchRun, fetchTask } from "@/util/data.js";
|
||||
|
||||
import Collapse from "@/components/collapse.vue";
|
||||
import RunDetail from "@/components/rundetail.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Collapse,
|
||||
RunDetail
|
||||
},
|
||||
name: "task",
|
||||
data() {
|
||||
return {
|
||||
run: null,
|
||||
task: null,
|
||||
runid: this.$route.params.runid,
|
||||
taskid: this.$route.params.taskid,
|
||||
polling: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
taskClass(task) {
|
||||
if (task.status == "success") return "is-success";
|
||||
if (task.status == "failed") return "is-failed";
|
||||
if (task.status == "stopped") return "is-failed";
|
||||
if (task.status == "running") return "is-running";
|
||||
return "unknown";
|
||||
},
|
||||
async fetchRun() {
|
||||
this.run = await fetchRun(this.runid);
|
||||
},
|
||||
async fetchTask() {
|
||||
this.task = await fetchTask(this.runid, this.taskid);
|
||||
},
|
||||
pollData() {
|
||||
this.polling = setInterval(() => {
|
||||
this.fetchTask();
|
||||
this.fetchRun();
|
||||
}, 2000);
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.fetchRun();
|
||||
this.fetchTask();
|
||||
this.pollData();
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.polling);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.task-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 5px;
|
||||
margin-bottom: 25px;
|
||||
|
||||
.task-name {
|
||||
padding-left: 5px;
|
||||
font-size: 1.5rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,143 @@
|
|||
// **
|
||||
// black
|
||||
// **
|
||||
|
||||
.ansi-black-fg {
|
||||
color: $ansi-black;
|
||||
}
|
||||
.ansi-black-bg {
|
||||
background-color: $ansi-black;
|
||||
}
|
||||
|
||||
.ansi-bright-black-fg {
|
||||
color: $ansi-black-bright;
|
||||
}
|
||||
.ansi-bright-black-bg {
|
||||
background-color: $ansi-black-bright;
|
||||
}
|
||||
|
||||
// **
|
||||
// red
|
||||
// **
|
||||
|
||||
.ansi-red-fg {
|
||||
color: $ansi-red;
|
||||
}
|
||||
.ansi-red-bg {
|
||||
background-color: $ansi-red;
|
||||
}
|
||||
|
||||
.ansi-bright-red-fg {
|
||||
color: $ansi-red-bright;
|
||||
}
|
||||
.ansi-bright-red-bg {
|
||||
background-color: $ansi-red-bright;
|
||||
}
|
||||
|
||||
// **
|
||||
// green
|
||||
// **
|
||||
|
||||
.ansi-green-fg {
|
||||
color: $ansi-green;
|
||||
}
|
||||
.ansi-green-bg {
|
||||
background-color: $ansi-green;
|
||||
}
|
||||
|
||||
.ansi-bright-green-fg {
|
||||
color: $ansi-green-bright;
|
||||
}
|
||||
.ansi-bright-green-bg {
|
||||
background-color: $ansi-green-bright;
|
||||
}
|
||||
|
||||
// **
|
||||
// yellow
|
||||
// **
|
||||
|
||||
.ansi-yellow-fg {
|
||||
color: $ansi-yellow;
|
||||
}
|
||||
.ansi-yellow-bg {
|
||||
background-color: $ansi-yellow;
|
||||
}
|
||||
|
||||
.ansi-bright-yellow-fg {
|
||||
color: $ansi-yellow-bright;
|
||||
}
|
||||
.ansi-bright-yellow-bg {
|
||||
background-color: $ansi-yellow-bright;
|
||||
}
|
||||
|
||||
// **
|
||||
// blue
|
||||
// **
|
||||
|
||||
.ansi-blue-fg {
|
||||
color: $ansi-blue;
|
||||
}
|
||||
.ansi-blue-bg {
|
||||
background-color: $ansi-blue;
|
||||
}
|
||||
|
||||
.ansi-bright-blue-fg {
|
||||
color: $ansi-blue-bright;
|
||||
}
|
||||
.ansi-bright-blue-bg {
|
||||
background-color: $ansi-blue-bright;
|
||||
}
|
||||
|
||||
// **
|
||||
// magenta
|
||||
// **
|
||||
|
||||
.ansi-magenta-fg {
|
||||
color: $ansi-magenta;
|
||||
}
|
||||
.ansi-magenta-bg {
|
||||
background-color: $ansi-magenta;
|
||||
}
|
||||
|
||||
.ansi-bright-magenta-fg {
|
||||
color: $ansi-magenta-bright;
|
||||
}
|
||||
.ansi-bright-magenta-bg {
|
||||
background-color: $ansi-magenta-bright;
|
||||
}
|
||||
|
||||
// **
|
||||
// cyan
|
||||
// **
|
||||
|
||||
.ansi-cyan-fg {
|
||||
color: $ansi-cyan;
|
||||
}
|
||||
.ansi-cyan-bg {
|
||||
background-color: $ansi-cyan;
|
||||
}
|
||||
|
||||
.ansi-bright-cyan-fg {
|
||||
color: $ansi-cyan-bright;
|
||||
}
|
||||
.ansi-bright-cyan-bg {
|
||||
background-color: $ansi-cyan-bright;
|
||||
}
|
||||
|
||||
// **
|
||||
// white
|
||||
// **
|
||||
|
||||
.ansi-white-fg {
|
||||
color: $ansi-white;
|
||||
}
|
||||
.ansi-white-bg {
|
||||
background-color: $ansi-white;
|
||||
}
|
||||
|
||||
.ansi-bright-white-fg {
|
||||
color: $ansi-white-bright;
|
||||
}
|
||||
.ansi-bright-white-bg {
|
||||
background-color: $ansi-white-bright;
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
@import "~bulma/sass/utilities/initial-variables.sass";
|
||||
@import "~bulma/sass/utilities/functions.sass";
|
||||
|
||||
$grey: #8c9b9d;
|
||||
$grey-light: #a9afb7;
|
||||
$grey-lighter: #dee2e5;
|
||||
$orange: #e67e22;
|
||||
$yellow: #f1b70e;
|
||||
$green: #2ecc71;
|
||||
$turquoise: #1abc9c;
|
||||
$blue: #3498db;
|
||||
$purple: #8e44ad;
|
||||
$red: #e42522;
|
||||
$white-ter: #ecf0f1;
|
||||
$primary: #34495e !default;
|
||||
$yellow-invert: #fff;
|
||||
|
||||
$grey-lighter-invert: findColorInvert($grey-lighter);
|
||||
$green-invert: findColorInvert($green);
|
||||
$red-invert: findColorInvert($red);
|
||||
$blue-invert: findColorInvert($red);
|
||||
|
||||
$custom-colors: (
|
||||
"unknown": (
|
||||
$grey-lighter,
|
||||
$grey-lighter-invert
|
||||
),
|
||||
"success": (
|
||||
$green,
|
||||
$green-invert
|
||||
),
|
||||
"failed": (
|
||||
$red,
|
||||
$red-invert
|
||||
),
|
||||
"running": (
|
||||
$blue,
|
||||
$blue-invert
|
||||
)
|
||||
);
|
||||
|
||||
$link: $grey;
|
||||
$tabs-link-active-color: $blue;
|
||||
$tabs-link-active-border-bottom-color: $blue;
|
||||
$tabs-link-hover-color: $blue;
|
||||
$tabs-link-hover-border-bottom-color: $blue;
|
||||
|
||||
$spacing: 20px;
|
||||
|
||||
$breadcrumb-item-color: $grey-dark !default;
|
||||
$breadcrumb-item-hover-color: $grey-dark !default;
|
||||
|
||||
$breadcrumb-item-padding-vertical: 0 !default;
|
||||
$breadcrumb-item-padding-horizontal: 0.5em !default;
|
||||
|
||||
$breadcrumb-item-separator-color: $grey-light !default;
|
||||
|
||||
$ansi-black: #222;
|
||||
$ansi-black-bright: #222;
|
||||
$ansi-red: #c0392b;
|
||||
$ansi-red-bright: #e74c3c;
|
||||
$ansi-green: #27af60;
|
||||
$ansi-green-bright: #2ecc71;
|
||||
$ansi-yellow: #f39c12;
|
||||
$ansi-yellow-bright: #f1c40f;
|
||||
$ansi-blue: #2e8dcd;
|
||||
$ansi-blue-bright: #3498db;
|
||||
$ansi-magenta: #8e44ad;
|
||||
$ansi-magenta-bright: #9b59b6;
|
||||
$ansi-cyan: #0097a4;
|
||||
$ansi-cyan-bright: #02c8d9;
|
||||
$ansi-white: #bdc3c7;
|
||||
$ansi-white-bright: #ffffff;
|
|
@ -0,0 +1,5 @@
|
|||
@import "@/css/_variables.scss";
|
||||
|
||||
@import "~bulma/bulma.sass";
|
||||
|
||||
@import "./css/_ansi.scss";
|
|
@ -0,0 +1,26 @@
|
|||
import "@mdi/font/css/materialdesignicons.css";
|
||||
|
||||
import Vue from "vue";
|
||||
import Vue2Filters from "vue2-filters";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import store from "./store";
|
||||
|
||||
import { getUser } from "@/util/auth";
|
||||
|
||||
Vue.use(Vue2Filters);
|
||||
|
||||
const USER = 'user';
|
||||
|
||||
// TODO(sgotti) use vuex for login/logout
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
created: function () {
|
||||
let user = getUser()
|
||||
if (user) {
|
||||
store.dispatch('setUser', user)
|
||||
}
|
||||
},
|
||||
render: h => h(App)
|
||||
}).$mount("#app");
|
|
@ -0,0 +1,149 @@
|
|||
import Vue from "vue";
|
||||
import VueRouter from "vue-router";
|
||||
import Home from "./views/Home.vue";
|
||||
import User from "./views/User.vue";
|
||||
import Org from "./views/Org.vue";
|
||||
import Project from "./views/Project.vue";
|
||||
//import Run from "./views/Run.vue";
|
||||
import projects from "./components/projects.vue";
|
||||
import runs from "./components/runs.vue";
|
||||
import run from "./components/run.vue";
|
||||
import task from "./components/task.vue";
|
||||
import Oauth2 from "./views/Oauth2.vue";
|
||||
import Login from "./views/Login.vue";
|
||||
import Logout from "./views/Logout.vue";
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
export default new VueRouter({
|
||||
mode: "history",
|
||||
routes: [
|
||||
{
|
||||
path: "/login",
|
||||
name: "login",
|
||||
component: Login
|
||||
},
|
||||
{
|
||||
path: "/logout",
|
||||
name: "logout",
|
||||
component: Logout
|
||||
},
|
||||
{
|
||||
path: "/oauth2/callback",
|
||||
name: "oauth2callback",
|
||||
component: Oauth2
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: "/user/:username",
|
||||
component: User,
|
||||
props: (route) => ({ username: route.params.username }),
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "user",
|
||||
component: projects,
|
||||
props: (route) => ({ ownertype: "user", ownername: route.params.username })
|
||||
},
|
||||
{
|
||||
path: "projects",
|
||||
name: "user projects",
|
||||
component: projects,
|
||||
props: (route) => ({ ownertype: "user", ownername: route.params.username })
|
||||
},
|
||||
{
|
||||
path: "runs",
|
||||
name: "user local runs",
|
||||
component: runs,
|
||||
props: (route) => ({ ownertype: "user", username: route.params.username })
|
||||
},
|
||||
{
|
||||
path: "runs/:runid",
|
||||
name: "user local run",
|
||||
component: run,
|
||||
props: (route) => ({ ownertype: "user", ownername: route.params.username, runid: route.params.runid })
|
||||
},
|
||||
{
|
||||
path: "runs/:runid/tasks/:taskid",
|
||||
name: "user local run task",
|
||||
component: task,
|
||||
props: (route) => ({ ownertype: "user", ownername: route.params.username, runid: route.params.runid, taskid: route.params.taskid })
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: "/user/:username/projects/:projectname",
|
||||
component: Project,
|
||||
props: (route) => ({ ownertype: "user", ownername: route.params.username, projectname: route.params.projectname }),
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "user project",
|
||||
component: runs,
|
||||
props: (route) => ({ ownertype: "user", ownername: route.params.username, projectname: route.params.projectname })
|
||||
},
|
||||
{
|
||||
path: "runs",
|
||||
name: "user project runs",
|
||||
component: runs,
|
||||
props: (route) => ({ ownertype: "user", ownername: route.params.username, projectname: route.params.projectname })
|
||||
},
|
||||
{
|
||||
path: "runs/:runid",
|
||||
name: "user project run",
|
||||
component: run,
|
||||
props: (route) => ({ ownertype: "user", ownername: route.params.username, projectname: route.params.projectname, runid: route.params.runid })
|
||||
},
|
||||
{
|
||||
path: "runs/:runid/tasks/:taskid",
|
||||
name: "user project run task",
|
||||
component: task,
|
||||
props: (route) => ({ ownertype: "user", ownername: route.params.username, projectname: route.params.projectname, runid: route.params.runid, taskid: route.params.taskid })
|
||||
},
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
path: "/org/:orgname",
|
||||
name: "org",
|
||||
component: Org,
|
||||
props: (route) => ({ orgname: route.params.orgname }),
|
||||
},
|
||||
|
||||
{
|
||||
path: "/org/:orgname/projects/:projectname",
|
||||
component: Project,
|
||||
props: (route) => ({ ownertype: "org", ownername: route.params.orgname, projectname: route.params.projectname }),
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "org project",
|
||||
component: runs,
|
||||
props: (route) => ({ ownertype: "org", ownername: route.params.orgname, projectname: route.params.projectname })
|
||||
},
|
||||
{
|
||||
path: "runs",
|
||||
name: "org project runs",
|
||||
component: runs,
|
||||
props: (route) => ({ ownertype: "org", ownername: route.params.orgname, projectname: route.params.projectname })
|
||||
},
|
||||
{
|
||||
path: "runs/:runid",
|
||||
name: "org project run",
|
||||
component: run,
|
||||
props: (route) => ({ ownertype: "org", ownername: route.params.orgname, projectname: route.params.projectname, runid: route.params.runid })
|
||||
},
|
||||
{
|
||||
path: "runs/:runid/tasks/:taskid",
|
||||
name: "org project run task",
|
||||
component: task,
|
||||
props: (route) => ({ ownertype: "org", ownername: route.params.orgname, projectname: route.params.projectname, runid: route.params.runid, taskid: route.params.taskid })
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
const state = {
|
||||
user: null,
|
||||
}
|
||||
|
||||
const getters = {
|
||||
user: state => {
|
||||
return state.user
|
||||
}
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
setUser(state, user) {
|
||||
state.user = user
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
setUser({ commit }, user) {
|
||||
commit('setUser', user)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default new Vuex.Store({
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
})
|
|
@ -0,0 +1,93 @@
|
|||
import router from "@/router";
|
||||
import store from "@/store";
|
||||
|
||||
const ID_TOKEN_KEY = 'id_token';
|
||||
const USER_KEY = 'user';
|
||||
|
||||
let API_URL = window.CONFIG.API_URL;
|
||||
let API_BASE_PATH = window.CONFIG.API_BASE_PATH;
|
||||
|
||||
export function login(token, user) {
|
||||
setIdToken(token);
|
||||
setUser(user);
|
||||
store.dispatch('setUser', user)
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
unsetIdToken();
|
||||
unsetUser()
|
||||
store.dispatch('setUser', null)
|
||||
}
|
||||
|
||||
export function apiurlwithtoken(path) {
|
||||
let u = new URL(API_URL + API_BASE_PATH + path);
|
||||
let idToken = getIdToken();
|
||||
if (idToken) {
|
||||
u.searchParams.append("access_token", idToken);
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
export function apiurl(path) {
|
||||
return new URL(API_URL + API_BASE_PATH + path);
|
||||
}
|
||||
|
||||
export function loginurl() {
|
||||
return new URL(API_URL + "/login");
|
||||
}
|
||||
|
||||
export function oauth2callbackurl() {
|
||||
return new URL(API_URL + "/oauth2/callback");
|
||||
}
|
||||
|
||||
export function fetch(url, init) {
|
||||
if (init === undefined) {
|
||||
init = {}
|
||||
}
|
||||
if (init.headers === undefined) {
|
||||
init["headers"] = {}
|
||||
}
|
||||
let idToken = getIdToken();
|
||||
if (idToken) {
|
||||
init.headers["Authorization"] = "bearer " + idToken
|
||||
}
|
||||
|
||||
return window.fetch(url, init).then(res => {
|
||||
if (res.status === 401) {
|
||||
router.push({ name: "login" })
|
||||
} else { return res }
|
||||
})
|
||||
}
|
||||
|
||||
export function setIdToken(idToken) {
|
||||
localStorage.setItem(ID_TOKEN_KEY, idToken);
|
||||
}
|
||||
|
||||
export function getIdToken() {
|
||||
return localStorage.getItem(ID_TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function unsetIdToken() {
|
||||
localStorage.removeItem(ID_TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function setUser(user) {
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
||||
}
|
||||
|
||||
export function getUser() {
|
||||
let user = localStorage.getItem(USER_KEY);
|
||||
if (user) {
|
||||
return JSON.parse(user)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function unsetUser() {
|
||||
localStorage.removeItem(USER_KEY);
|
||||
}
|
||||
|
||||
export function isLoggedIn() {
|
||||
const idToken = getIdToken();
|
||||
return !!idToken;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { apiurl, fetch } from "@/util/auth";
|
||||
|
||||
export async function fetchRun(runid) {
|
||||
let res = await fetch(apiurl("/run/" + runid));
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function fetchTask(runid, taskid) {
|
||||
let res = await fetch(apiurl("/run/" + runid + "/task/" + taskid))
|
||||
return res.json();
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
|
||||
export function ownerLink(ownertype, ownername) {
|
||||
if (ownertype == "user") {
|
||||
return { name: ownertype, params: { username: ownername } }
|
||||
} else if (ownertype == "org") {
|
||||
return { name: ownertype, params: { orgname: ownername } }
|
||||
}
|
||||
}
|
||||
|
||||
export function ownerProjectsLink(ownertype, ownername) {
|
||||
return { name: ownertype + " projects", params: { ownername: ownername } }
|
||||
}
|
||||
|
||||
export function userLocalRunsLink(username) {
|
||||
return { name: "user local runs", params: { username: username } }
|
||||
}
|
||||
|
||||
export function userLocalRunLink(username, runid) {
|
||||
return { name: "user local run", params: { username: username, runid: runid } }
|
||||
}
|
||||
|
||||
export function userLocalRunTaskLink(username, runid, taskid) {
|
||||
return { name: "user local run task", params: { username: username, runid: runid, taskid: taskid } }
|
||||
}
|
||||
|
||||
export function projectLink(ownertype, ownername, projectname) {
|
||||
return { name: ownertype + " project", params: { username: ownername, projectname: projectname } }
|
||||
}
|
||||
|
||||
export function projectRunsLink(ownertype, ownername, projectname) {
|
||||
return { name: ownertype + " project runs", params: { orgname: ownername, projectname: projectname } }
|
||||
}
|
||||
|
||||
export function projectRunLink(ownertype, ownername, projectname, runid) {
|
||||
return { name: ownertype + " project run", params: { orgname: ownername, projectname: projectname, runid: runid } }
|
||||
}
|
||||
|
||||
export function projectRunTaskLink(ownertype, ownername, projectname, runid, taskid) {
|
||||
return { name: ownertype + " project run task", params: { orgname: ownername, projectname: projectname, runid: runid, taskid: taskid } }
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<div class="home"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
|
||||
export default {
|
||||
name: "Home",
|
||||
components: {},
|
||||
computed: {
|
||||
...mapGetters(["user"])
|
||||
},
|
||||
created: function() {
|
||||
let user = this.$store.getters.user;
|
||||
if (user) {
|
||||
this.$router.push({
|
||||
name: "user",
|
||||
params: { username: this.user.username }
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="column is-4 is-offset-4">
|
||||
<div class="box" v-for="rs in remotesources" v-bind:key="rs.id">
|
||||
<Loginform
|
||||
:name="rs.name"
|
||||
v-if="rs.auth_type == 'password'"
|
||||
v-on:login="doLogin(rs.name, $event.username, $event.password)"
|
||||
/>
|
||||
<button
|
||||
v-else
|
||||
class="button is-info is-fullwidth"
|
||||
@click="doLogin(rs.name)"
|
||||
>Login with {{rs.name}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Loginform from "@/components/loginform";
|
||||
import { apiurl, loginurl, fetch, login, logout } from "@/util/auth";
|
||||
|
||||
export default {
|
||||
name: "Login",
|
||||
components: {
|
||||
Loginform
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
remotesources: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getRemoteSources() {
|
||||
fetch(apiurl("/remotesources"))
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
console.log("remote sources result", res);
|
||||
this.remotesources = res.remote_sources;
|
||||
});
|
||||
},
|
||||
doLogin(rsName, username, password) {
|
||||
let u = loginurl();
|
||||
console.log("u:", u);
|
||||
fetch(u, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
remote_source_name: rsName,
|
||||
login_name: username,
|
||||
password: password
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
console.log("login result", res);
|
||||
if (res.oauth2_redirect) {
|
||||
window.location = res.oauth2_redirect;
|
||||
return;
|
||||
}
|
||||
login(res.token, res.user);
|
||||
this.$router.push({ name: "home" });
|
||||
});
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
logout();
|
||||
this.getRemoteSources();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<template></template>
|
||||
|
||||
<script>
|
||||
import { logout } from "@/util/auth";
|
||||
|
||||
export default {
|
||||
name: "Logout",
|
||||
created: function() {
|
||||
logout();
|
||||
this.$router.push("/");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<template>
|
||||
<div>
|
||||
<div>{{code}}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { apiurl, oauth2callbackurl, fetch, setUser } from "@/util/auth";
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
name: "Oauth2",
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
run: null,
|
||||
code: this.$route.query.code,
|
||||
polling: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
doOauth2() {
|
||||
let u = oauth2callbackurl();
|
||||
u.searchParams.append("code", this.$route.query.code);
|
||||
u.searchParams.append("state", this.$route.query.state);
|
||||
fetch(u)
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
console.log("oauth2 result", res);
|
||||
if (res.request_type === "loginuser") {
|
||||
this.$root.login(res.response.token, res.response.user);
|
||||
this.$router.push("/");
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.doOauth2();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="org-title">
|
||||
<span class="org-name">{{orgname}}</span>
|
||||
</div>
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
<li :class="[{ 'is-active': currentTab === 'projects' }]">
|
||||
<a @click="currentTab = 'projects'">Projects</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<projects v-if="currentTab == 'projects'" ownertype="org" :ownername="orgname"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import projects from "@/components/projects.vue";
|
||||
|
||||
export default {
|
||||
name: "Org",
|
||||
components: { projects },
|
||||
props: {
|
||||
orgname: String,
|
||||
currentTab: {
|
||||
type: String,
|
||||
default: "projects"
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/css/_variables.scss";
|
||||
|
||||
.org-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 5px;
|
||||
margin-bottom: 25px;
|
||||
.org-name {
|
||||
padding-left: 5px;
|
||||
font-size: 1.5rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,90 @@
|
|||
<template>
|
||||
<div>
|
||||
<projbreadcrumbs :ownertype="ownertype" :ownername="ownername" :projectname="projectname"/>
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
<li
|
||||
:class="[{ 'is-active': $route.name.endsWith('project runs') || $route.name.endsWith('project') }]"
|
||||
>
|
||||
<router-link :to="projectRunsLink(ownertype, ownername, projectname)">Runs</router-link>
|
||||
</li>
|
||||
<li
|
||||
v-if="$route.name.endsWith('project run') || $route.name.endsWith('project run task')"
|
||||
:class="[{ 'is-active': $route.name.endsWith('project run') }]"
|
||||
>
|
||||
<tabarrow/>
|
||||
</li>
|
||||
<li
|
||||
v-if="$route.name.endsWith('project run') || $route.name.endsWith('project run task')"
|
||||
:class="[{ 'is-active': $route.name.endsWith('project run') }]"
|
||||
>
|
||||
<router-link
|
||||
:to="projectRunLink(ownertype, ownername, $route.params.projectname, $route.params.runid)"
|
||||
>Run {{$route.params.runid}}</router-link>
|
||||
</li>
|
||||
<li
|
||||
v-if="$route.name.endsWith('project run task')"
|
||||
:class="[{ 'is-active': $route.name.endsWith('project run') }]"
|
||||
>
|
||||
<tabarrow/>
|
||||
</li>
|
||||
<li v-if="$route.name.endsWith('project run task')" class="is-active">
|
||||
<router-link
|
||||
:to="projectRunTaskLink(ownertype, ownername, $route.params.projectname, $route.params.runid, $route.params.taskid)"
|
||||
>Task {{$route.params.taskid}}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import {
|
||||
projectLink,
|
||||
projectRunsLink,
|
||||
projectRunLink,
|
||||
projectRunTaskLink
|
||||
} from "@/util/link.js";
|
||||
|
||||
import projbreadcrumbs from "@/components/projbreadcrumbs.vue";
|
||||
import runs from "@/components/runs.vue";
|
||||
import tabarrow from "@/components/tabarrow.vue";
|
||||
|
||||
export default {
|
||||
name: "Project",
|
||||
components: { projbreadcrumbs, runs, tabarrow },
|
||||
props: {
|
||||
ownertype: String,
|
||||
ownername: String,
|
||||
projectname: String,
|
||||
currentTab: {
|
||||
type: String,
|
||||
default: "description"
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
projectLink: projectLink,
|
||||
projectRunsLink: projectRunsLink,
|
||||
projectRunLink: projectRunLink,
|
||||
projectRunTaskLink: projectRunTaskLink
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/css/_variables.scss";
|
||||
|
||||
.user-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 5px;
|
||||
margin-bottom: 25px;
|
||||
.user-name {
|
||||
padding-left: 5px;
|
||||
font-size: 1.5rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,120 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="user-title">
|
||||
<router-link class="user-name" :to="ownerLink('user', username)">
|
||||
<span>{{username}}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
<li :class="[{ 'is-active': $route.name === 'user projects' || $route.name === 'user' }]">
|
||||
<router-link :to="ownerProjectsLink('user', username)">Projects</router-link>
|
||||
</li>
|
||||
<li :class="[{ 'is-active': $route.name === 'user local runs' }]">
|
||||
<router-link :to="userLocalRunsLink(username)">Local Runs</router-link>
|
||||
</li>
|
||||
<li
|
||||
v-if="$route.name === 'user local run' || $route.name == 'user local run task'"
|
||||
:class="[{ 'is-active': $route.name === 'user local run' }]"
|
||||
>
|
||||
<tabarrow/>
|
||||
</li>
|
||||
<li
|
||||
v-if="$route.name === 'user local run' || $route.name == 'user local run task'"
|
||||
:class="[{ 'is-active': $route.name === 'user local run' }]"
|
||||
>
|
||||
<router-link :to="userLocalRunLink(username, $route.params.runid)">
|
||||
<p v-if="run">
|
||||
Run
|
||||
<strong>#{{run.counter}}</strong>
|
||||
</p>
|
||||
</router-link>
|
||||
</li>
|
||||
<li
|
||||
v-if="$route.name === 'user local run task'"
|
||||
:class="[{ 'is-active': $route.name === 'user local run' }]"
|
||||
>
|
||||
<tabarrow/>
|
||||
</li>
|
||||
<li v-if="$route.name == 'user local run task'" class="is-active">
|
||||
<router-link
|
||||
:to="userLocalRunTaskLink(username, $route.params.runid, $route.params.taskid)"
|
||||
>
|
||||
<p v-if="run">
|
||||
Task
|
||||
<strong>{{run.tasks[$route.params.taskid].name}}</strong>
|
||||
</p>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { apiurl, fetch } from "@/util/auth";
|
||||
import {
|
||||
ownerLink,
|
||||
ownerProjectsLink,
|
||||
userLocalRunsLink,
|
||||
userLocalRunLink,
|
||||
userLocalRunTaskLink
|
||||
} from "@/util/link.js";
|
||||
|
||||
import { fetchRun } from "@/util/data.js";
|
||||
|
||||
import tabarrow from "@/components/tabarrow.vue";
|
||||
|
||||
export default {
|
||||
name: "User",
|
||||
components: { tabarrow },
|
||||
props: {
|
||||
username: String,
|
||||
currentTab: {
|
||||
type: String,
|
||||
default: "projects"
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
run: null
|
||||
};
|
||||
},
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
if (!to.params.runid) next();
|
||||
let run = await fetchRun(to.params.runid);
|
||||
next(vm => (vm.run = run));
|
||||
},
|
||||
async beforeRouteUpdate(to, from, next) {
|
||||
if (!to.params.runid) next();
|
||||
this.run = await fetchRun(to.params.runid);
|
||||
next();
|
||||
},
|
||||
methods: {
|
||||
ownerLink: ownerLink,
|
||||
ownerProjectsLink: ownerProjectsLink,
|
||||
userLocalRunsLink: userLocalRunsLink,
|
||||
userLocalRunLink: userLocalRunLink,
|
||||
userLocalRunTaskLink: userLocalRunTaskLink
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/css/_variables.scss";
|
||||
|
||||
.user-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 5px;
|
||||
margin-bottom: 25px;
|
||||
.user-name {
|
||||
color: $grey-dark;
|
||||
padding-left: 5px;
|
||||
font-size: 1.5rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,7 @@
|
|||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
css: {
|
||||
sourceMap: true
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue