Merge pull request #61 from vincent99/master

Stuff
This commit is contained in:
Vincent Fiduccia 2019-11-06 18:44:29 -07:00 committed by GitHub
commit c05ec33160
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 538 additions and 123 deletions

View File

@ -15,9 +15,12 @@ BODY {
direction: ltr;
position: relative;
margin: 0;
overflow: hidden;
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
&.overflow-hidden {
overflow: hidden;
}
}
::-webkit-scrollbar {

View File

@ -34,6 +34,12 @@ const LABEL = {
waiting: 'Saving…',
success: 'Saved',
error: 'Error',
},
done: {
action: 'Done',
waiting: 'Saving…',
success: 'Saved',
error: 'Error',
}
};

View File

@ -1,11 +1,13 @@
<script>
import { mapState } from 'vuex';
import { debounce } from 'lodash';
import { addParam, parseLinkHeader } from '@/utils/url';
import { addObjects, isArray } from '@/utils/array';
import { EXTENDED_SCOPES } from '@/store/auth';
import { findBy } from '../utils/array';
import { EXTENDED_SCOPES } from '@/store/github';
const API_BASE = 'https://api.github.com/';
export const FILE_PATTERNS = {
'dockerfile': /^Dockerfile(\..*)?$/i,
'yaml': /^.*\.ya?ml?$/i,
};
export default {
props: {
@ -14,19 +16,20 @@ export default {
required: true,
},
// filter files displayed in dropdown - default to .yml and Dockerfile files
// Filter files displayed in dropdown by keys in FILE_PATTERNS
filePattern: {
type: RegExp,
default: () => {
return new RegExp('\.ya?ml$|^Dockerfile(\..*)?', 'i');
}
type: String,
default: null,
},
preferredFile: {
type: String,
default: null,
}
},
data() {
return {
scopes: [],
recentRepos: [],
repos: [],
branches: [],
files: [],
@ -42,8 +45,45 @@ export default {
},
computed: {
...mapState({
scopes: state => state.github.scopes,
recentRepos: state => state.github.repos,
}),
hasPrivate() {
return this.scopes.includes('repo');
},
repoPlaceholder() {
if ( this.loadingRecentRepos ) {
return 'Loading...';
} else {
return 'Select a Repository...';
}
},
branchPlaceholder() {
if ( this.selectedRepo ) {
if ( this.loadingBranches ) {
return 'Loading...';
} else {
return 'Select a Branch...';
}
} else {
return 'Select a Repository First';
}
},
filePlaceholder() {
if ( this.selectedBranch ) {
if ( this.loadingFiles ) {
return 'Loading...';
} else {
return 'Select a File...';
}
} else {
return 'Select a Branch First';
}
}
},
@ -56,14 +96,22 @@ export default {
},
methods: {
update() {
if ( this.selectedRepo && this.selectedBranch && this.selectedFile ) {
// Do something
}
},
selectRepo(repo) {
this.selectedFile = null;
this.selectedBranch = null;
this.selectedRepo = repo;
this.$emit('selectedRepo', repo);
this.fetchBranches(repo);
this.update();
},
selectBranch(branch) {
this.selectedFile = null;
this.selectedBranch = branch;
this.$emit('selectedBranch', branch);
this.fetchFiles(this.selectedRepo, branch);
@ -96,7 +144,8 @@ export default {
try {
loading(true);
const res = await this.apiList(`/search/repositories?q=${ escape(search) }`, { depaginate: false });
const res = await this.$store.dispatch('github/searchRepos', { search });
this.repos = res;
} catch (err) {
@ -108,12 +157,9 @@ export default {
async fetchRepos() {
try {
if ( !this.recentRepos.length) {
const res = await this.apiList('/user/repos?sort=updated', { depaginate: false });
const res = await this.$store.dispatch('github/fetchRecentRepos');
this.recentRepos = res;
this.repos = res.slice();
}
this.repos = res;
} finally {
this.loadingRecentRepos = false;
}
@ -122,69 +168,43 @@ export default {
async fetchBranches(repo) {
this.loadingBranches = true;
const url = repo.branches_url.replace('{/branch}', '');
const res = await this.apiList(url);
try {
const res = await this.$store.dispatch('github/fetchBranches', { repo: this.selectedRepo });
this.branches = res;
if ( !this.selectedBranch ) {
const master = findBy(this.branches, 'name', 'master');
if ( master ) {
this.selectBranch(master);
}
}
} finally {
this.loadingBranches = false;
}
},
async fetchFiles(repo, branch) {
let url = repo.trees_url.replace('{/sha}', `/${ branch.commit.sha }`);
try {
const res = await this.$store.dispatch('github/fetchFiles', {
repo: this.selectedRepo,
branch: this.selectedBranch,
pattern: FILE_PATTERNS[(this.filePattern || '').toLowerCase()],
});
url = addParam(url, 'recursive', 1);
this.files = res;
const res = await this.apiList(url, { objectKey: 'tree' });
if ( !this.selecteeFile && this.preferredFile ) {
const file = findBy(this.files, 'path', this.preferredFile);
this.files = res.filter(file => file.type === 'blob' && file.path.match(this.filePattern));
if ( file ) {
this.selectFile(file);
}
}
} finally {
this.loadingFiles = false;
},
proxifyUrl(url) {
// Strip off absolute links to github API
if ( url.startsWith(API_BASE) ) {
url = url.substr(API_BASE.length);
}
// Add our proxy prefix
url = `/v1/github/${ url.replace(/^\/+/, '') }`;
// Less pages please
addParam(url, 'per_page', 100);
return url;
},
async apiList(url, { depaginate = true, onPageFn = null, objectKey = 'items' } = {}) {
const out = [];
url = this.proxifyUrl(url);
while ( true ) {
console.log('Github Request:', url);
const res = await this.$store.dispatch('rancher/request', { url });
const links = parseLinkHeader(res._headers['link']);
const scopes = res._headers['x-oauth-scopes'];
if ( scopes ) {
this.scopes = scopes;
}
addObjects(out, isArray(res) ? res : res[objectKey]);
if ( onPageFn ) {
onPageFn(out);
}
if ( depaginate && links.next ) {
url = this.proxifyUrl(links.next);
} else {
break;
}
}
return out;
},
},
};
@ -206,7 +226,7 @@ export default {
<div class="row">
<div class="col span-4">
<v-select
:placeholder="loadingRecentRepos ? 'Loading...' : 'Choose a Repository...'"
:placeholder="repoPlaceholder"
:disabled="loadingRecentRepos"
:options="repos"
label="full_name"
@ -240,7 +260,7 @@ export default {
<div class="col span-4">
<v-select
:disabled="!selectedRepo || loadingBranches"
placeholder="Choose branch"
:placeholder="branchPlaceholder"
:options="branches"
label="name"
:value="selectedBranch"
@ -252,7 +272,7 @@ export default {
<div class="col span-4">
<v-select
:disabled="!selectedBranch"
placeholder="Choose file"
:placeholder="filePlaceholder"
:options="files"
label="path"
:value="selectedFile"

View File

@ -189,7 +189,12 @@ export default {
<div v-if="buildMode === 'github'" class="row">
<div class="col span-12">
<GithubPicker v-model="spec.build" file-key="dockefile" />
<GithubPicker
v-model="spec.build"
file-key="dockefile"
file-pattern="Dockerfile"
preferred-file="Dockerfile"
/>
</div>
</div>

View File

@ -52,7 +52,7 @@ export default {
<div class="text-center">
<AsyncButton v-if="isEdit" key="edit" mode="edit" @click="save" />
<AsyncButton v-if="isCreate" key="create" mode="create" @click="save" />
<button v-if="!isView" class="btn bg-transparent" @click="done">
<button v-if="!isView" type="button" class="btn bg-transparent" @click="done">
Cancel
</button>
</div>

View File

@ -0,0 +1,56 @@
<script>
export default {
props: {
value: {
type: Array,
default: null,
},
row: {
type: Object,
required: true
},
col: {
type: Object,
required: true
},
},
computed: {
bestLink() {
if ( this.value && this.value.length ) {
return this.value[0];
}
return null;
},
protocol() {
const link = this.bestLink;
if ( link ) {
const match = link.match(/^([^:]+):\/\//);
if ( match ) {
return match[1];
} else {
return 'link';
}
}
return null;
}
},
};
</script>
<template>
<span>
<a v-if="bestLink" :href="bestLink" target="_blank" rel="nofollow noopener noreferrer">
{{ protocol }}
</a>
<span v-else class="text-muted">
&mdash;
</span>
</span>
</template>

View File

@ -25,18 +25,24 @@ export default {
let desired = 0;
let current = 0;
let count = 0;
for ( const service of forThisApp ) {
const weights = service.weights;
desired += weights.desired || 0;
current += weights.current || 0;
count++;
}
desired = Math.max(1, desired);
current = Math.max(1, current);
return { desired, current };
return {
desired,
current,
count
};
},
desired() {
@ -67,7 +73,10 @@ export default {
<template>
<div>
<span v-trim-whitespace :class="{'text-muted': current === 100 && desired === 100}">
<span v-if="total.count === 1" class="text-muted">
&mdash;
</span>
<span v-else v-trim-whitespace :class="{'text-muted': current === 0 && desired === 0}">
{{ current }}%
</span>
<div v-if="showDesired">

View File

@ -2,7 +2,7 @@ import { CONFIG_MAP, SECRET, RIO } from '@/config/types';
import {
STATE, NAME, NAMESPACE_NAME, AGE,
RIO_IMAGE, WEIGHT, SCALE,
KEYS,
KEYS, ENDPOINTS,
TARGET, TARGET_KIND,
} from '@/config/table-headers';
@ -58,6 +58,7 @@ export const FRIENDLY = {
STATE,
NAMESPACE_NAME,
RIO_IMAGE,
ENDPOINTS,
WEIGHT,
SCALE,
AGE,

3
config/local-storage.js Normal file
View File

@ -0,0 +1,3 @@
// Github repo cache
export const GITHUB_REPOS = 'githubRepos';
export const _DATE = 'Updated';

View File

@ -5,7 +5,7 @@ export const STATE = {
label: 'State',
sort: ['stateSort', 'nameSort'],
value: 'stateDisplay',
width: 90,
width: 75,
default: 'unknown',
formatter: 'BadgeState',
};
@ -55,8 +55,8 @@ export const AGE = {
value: 'metadata.creationTimestamp',
sort: ['createdTs', 'nameSort'],
search: false,
width: 75,
formatter: 'LiveDate',
width: 75,
align: 'right'
};
@ -67,13 +67,22 @@ export const RIO_IMAGE = {
sort: ['imageDisplay', 'nameSort'],
};
export const ENDPOINTS = {
name: 'endpoint',
label: 'Endpoint',
value: 'status.endpoints',
formatter: 'Endpoints',
width: 60,
align: 'center',
};
export const SCALE = {
name: 'scale',
label: 'Scale',
value: 'scales.desired',
sort: ['scales.desired', 'nameSort'],
width: 100,
formatter: 'Scale',
width: 60,
align: 'center',
};
@ -81,9 +90,10 @@ export const WEIGHT = {
name: 'weight',
label: 'Weight',
value: 'status.computedWeight',
width: 100,
align: 'center',
sort: 'status.computedWeight',
formatter: 'Weight',
width: 60,
align: 'center',
};
export const SUCCESS = {

View File

@ -30,7 +30,7 @@ export default {
const theme = this.$store.getters['prefs/get'](THEME);
return {
bodyAttrs: { class: `theme-${ theme }` },
bodyAttrs: { class: `theme-${ theme } overflow-hidden` },
title: 'Rio Dashboard',
};
},

View File

@ -15,5 +15,15 @@ export default {
return out;
}
},
principalType() {
const parts = this.id.replace(/:.*$/, '').split('_', 2);
if ( parts.length === 2 ) {
return parts[1];
}
return null;
}
};

View File

@ -76,11 +76,11 @@ export default {
if ( global ) {
desired = current;
} else if ( this._local.pendingScale >= 0 ) {
} else if ( typeof this._local.pendingScale === 'number' ) {
desired = this._local.pendingScale;
}
const missing = desired - available - unavailable;
const missing = Math.max(0, desired - available - unavailable);
return {
hasStatus,
@ -158,9 +158,11 @@ export default {
if ( this._local.scaleTimer ) {
scale = this._local.pendingScale;
} else {
scale = this.spec.scale;
scale = this.scales.desired;
}
scale = scale || 0;
this._local.pendingScale = scale + 1;
this.saveScale();
};
@ -174,12 +176,14 @@ export default {
return;
}
if ( this.this._local.scaleTimer ) {
if ( this._local.scaleTimer ) {
scale = this._local.pendingScale;
} else {
scale = this.spec.scale;
scale = this.scales.desired;
}
scale = scale || 1;
this._local.pendingScale = Math.max(scale - 1, 0);
this.saveScale();
};
@ -193,11 +197,11 @@ export default {
this._local.scaleTimer = setTimeout(async() => {
try {
await this.patch({
await this.patch([{
op: 'replace',
path: '/spec/scale',
path: '/spec/replicas',
value: this._local.pendingScale
});
}]);
} catch (err) {
this.$dispatch('growl/fromError', { title: 'Error updating scale', err }, { root: true });
}

View File

@ -6,6 +6,7 @@ import AsyncButton from '@/components/AsyncButton';
import { SETUP, STEP, _DELETE } from '@/config/query-params';
import { RANCHER } from '@/config/types';
import { open, popupWindowOptions } from '@/utils/window';
import { findBy, filterBy, addObject } from '@/utils/array';
export default {
layout: 'plain',
@ -15,6 +16,15 @@ export default {
},
computed: {
telemetryTooltip() {
return `Rancher Labs would like to collect a bit of anonymized information<br/>
about the configuration of your installation to help make Rio better.<br/></br>
Your data will not be shared with anyone else, and no information about<br/>
what specific resources or endpoints you are deploying is included.<br/>
Once enabled you can view exactly what data will be sent at <code>/v1-telemetry</code>.<br/><br/>
<a href="https://rancher.com/docs/rancher/v2.x/en/faq/telemetry/" target="_blank">More Info</a>`;
},
passwordSubmitDisabled() {
if ( this.useRandom ) {
return false;
@ -38,6 +48,18 @@ export default {
return false;
},
me() {
const out = findBy(this.prinicipals, 'me', true);
return out;
},
orgs() {
const out = filterBy(this.principals, 'principalType', 'org');
return out;
}
},
async asyncData({ route, req, store }) {
@ -56,12 +78,19 @@ export default {
opt: { url: '/v3/settings/telemetry-opt' }
});
const githubConfig = await store.dispatch('rancher/find', {
let githubConfig = await store.dispatch('rancher/find', {
type: RANCHER.AUTH_CONFIG,
id: 'github',
opt: { url: '/v3/authConfigs/github' }
});
githubConfig = await store.dispatch('rancher/clone', githubConfig);
const principals = await store.dispatch('rancher/findAll', {
type: RANCHER.PRINCIPAL,
opt: { url: '/v3/principals' }
});
let origin;
let serverUrl = serverUrlSetting.value;
@ -110,6 +139,8 @@ export default {
hostname: githubConfig.hostname || 'github.com',
tls: kind === 'public' || githubConfig.tls,
githubError: null,
principals
};
},
@ -222,6 +253,25 @@ export default {
description: 'Initial setup session',
});
const githubConfig = await this.$store.dispatch('rancher/find', {
type: RANCHER.AUTH_CONFIG,
id: 'github',
opt: { url: '/v3/authConfigs/github' }
});
this.githubConfig = await this.$store.dispatch('rancher/clone', githubConfig);
this.githubConfig.allowedPrincipalIds = this.githubConfig.allowedPrincipalIds || [];
if ( this.me ) {
addObject(this.githubConfig.allowedPrincipalIds, this.me.id);
}
this.principals = await this.$store.dispatch('rancher/findAll', {
type: RANCHER.PRINCIPAL,
opt: { url: '/v3/principals' }
});
buttonCb(true);
this.step = 4;
this.$router.applyQuery({ [STEP]: this.step });
@ -231,7 +281,19 @@ export default {
}
},
skipGithub() {
async setAuthorized(buttonCb) {
try {
window.z = this.githubConfig;
console.log(this.githubConfig);
await this.githubConfig.save();
buttonCb(true);
this.done();
} catch (e) {
buttonCb(false);
}
},
done() {
this.$router.replace('/');
},
},
@ -341,16 +403,11 @@ export default {
<div class="col span-6 offset-3">
<div class="checkbox">
<label>
<input v-model="telemetry" type="checkbox" disabled />
Allow collection of anonymous statistics (required during beta period).
<input v-model="telemetry" type="checkbox" />
Allow collection of anonymous statistics
</label>
<i v-tooltip="{content: telemetryTooltip, placement: 'right', trigger: 'click'}" class="icon icon-info" />
</div>
<p>
Rancher Labs would like to collect anonymous information about
the configuration of your installation to help make Rio better.
Your data will not be shared with anyone else, and no specific
resource names or addresses are collected.
</p>
</div>
</div>
@ -452,7 +509,7 @@ export default {
<div class="row mt-20">
<div class="col span-6 offset-3 text-center" style="font-size: 24pt">
<button type="button" class="btn bg-default" @click="skipGithub">
<button type="button" class="btn bg-default" @click="done">
Skip
</button>
<AsyncButton key="githubSubmit" mode="continue" :disabled="serverSubmitDisabled" @click="testGithub" />
@ -461,7 +518,58 @@ export default {
</div>
<div v-if="step === 4">
Allowed Principals...
<div class="text-center mb-40">
<div style="width: 50%; margin: 50px auto; height: 300px; padding-top: 100px; border: 1px solid var(--border); text-align: center; background-color: var(--border);">
<h1>An even snazzier picture, with octocats</h1>
</div>
<h1>GitHub Integration, Part Deux</h1>
<p class="m-20">
Who should be able to login?
</p>
</div>
<div class="row">
<div class="col span-4 offset-4">
<label v-if="me" class="principal">
<input type="checkbox" checked disabled />
<img :src="me.avatarSrc" width="40" height="40" />
<div class="login">
{{ me.loginName }}
</div>
<div class="name">
{{ me.name }}
</div>
</label>
<label v-for="org in orgs" :key="org.id" class="principal">
<input v-model="githubConfig.allowedPrincipalIds" type="checkbox" :value="org.id" />
<img :src="org.avatarSrc" width="40" height="40" />
<span class="login">
Members of <b>{{ org.loginName }}</b>
</span>
</label>
</div>
</div>
</div>
<div class="row mt-20">
<div class="col span-6 offset-3 text-center" style="font-size: 24pt">
<AsyncButton key="githubSubmit" mode="done" @click="setAuthorized" />
</div>
</div>
</form>
</template>
<style lang="scss" scoped>
.principal {
display: block;
border: 1px solid var(--border);
margin: 10px 0;
padding: 10px;
line-height: 40px;
img {
vertical-align: middle;
margin: 0 10px;
}
}
</style>

View File

@ -416,9 +416,9 @@ export default {
},
save() {
return (opt = {}) => {
delete this.__rehydrate;
return (opt = {}) => {
if ( !opt.url ) {
opt.url = this.linkFor('self');
}

View File

@ -3,11 +3,9 @@ import { randomStr } from '@/utils/string';
import { parse as parseUrl, addParam, addParams } from '@/utils/url';
import { findBy, addObjects } from '@/utils/array';
import { BACK_TO, SPA, AUTH_TEST, _FLAGGED } from '@/config/query-params';
import { BASE_SCOPES } from '@/store/github';
const KEY = 'rc_nonce';
const BASE_SCOPES = ['read:user', 'read:org', 'user:email'];
export const EXTENDED_SCOPES = ['repo'];
const ERR_NONCE = 'nonce';
const ERR_CLIENT = 'client';

182
store/github.js Normal file
View File

@ -0,0 +1,182 @@
import dayjs from 'dayjs';
import { addParam, parseLinkHeader } from '@/utils/url';
import { addObjects, isArray } from '@/utils/array';
import { GITHUB_REPOS, _DATE } from '@/config/local-storage';
const API_BASE = 'https://api.github.com/';
export const BASE_SCOPES = ['read:user', 'read:org', 'user:email'];
export const EXTENDED_SCOPES = ['repo'];
export const DOCKERFILE = /^Dockerfile(\..*)?$/i;
export const YAML_FILE = /^.*\.ya?ml$/i;
function getCached() {
if ( process.server ) {
return [];
}
const cached = window.localStorage.getItem(GITHUB_REPOS);
if ( cached ) {
try {
const parsed = JSON.parse(cached);
return parsed;
} catch (e) {}
}
return [];
}
function hasCached() {
if ( process.server ) {
return false;
}
const cached = window.localStorage.getItem(GITHUB_REPOS);
return !!cached;
}
function cacheExpired() {
if ( process.server ) {
return false;
}
const updated = window.localStorage.getItem(GITHUB_REPOS + _DATE);
if ( updated && dayjs().diff(updated) <= 60 * 60 * 1000 ) {
return false;
}
return true;
}
function setCache(repos) {
window.localStorage.setItem(GITHUB_REPOS, JSON.stringify(repos));
window.localStorage.setItem(GITHUB_REPOS + _DATE, (new Date()).toISOString());
}
function proxifyUrl(url) {
// Strip off absolute links to github API
if ( url.startsWith(API_BASE) ) {
url = url.substr(API_BASE.length);
}
// Add our proxy prefix
url = `/v1/github/${ url.replace(/^\/+/, '') }`;
// Less pages please
addParam(url, 'per_page', 100);
return url;
}
export const state = function() {
return {
repos: [],
scopes: []
};
};
export const actions = {
async apiList({ commit, dispatch }, {
url = null, depaginate = true, onPageFn = null, objectKey = 'items'
} = {}) {
const out = [];
url = proxifyUrl(url);
while ( true ) {
console.log('Github Request:', url);
const res = await dispatch('rancher/request', { url }, { root: true });
const links = parseLinkHeader(res._headers['link']);
const scopes = res._headers['x-oauth-scopes'];
if ( scopes ) {
commit('setScopes', scopes);
}
addObjects(out, isArray(res) ? res : res[objectKey]);
if ( onPageFn ) {
onPageFn(out);
}
if ( depaginate && links.next ) {
url = proxifyUrl(links.next);
} else {
break;
}
}
return out;
},
async fetchRecentRepos({ commit, dispatch }, { allowCache = true } = {}) {
if ( allowCache && hasCached ) {
const cached = getCached();
if ( cacheExpired() ) {
dispatch('fetchRecentRepos', { allowCache: false });
}
return cached;
}
const res = await dispatch('apiList', { url: '/user/repos?sort=updated', depaginate: false });
commit('setRepos', res.slice());
return res;
},
async searchRepos({ state, dispatch }, { search }) {
if ( !search ) {
return state.repos.slice();
}
const res = await dispatch('apiList', {
url: `/search/repositories?q=${ escape(search) }`,
depaginate: false
});
return res;
},
async fetchBranches({ dispatch }, { repo }) {
const url = repo.branches_url.replace('{/branch}', '');
const res = await dispatch('apiList', { url });
return res;
},
async fetchFiles({ dispatch }, { repo, branch, pattern = null }) {
let url = repo.trees_url.replace('{/sha}', `/${ branch.commit.sha }`);
url = addParam(url, 'recursive', 1);
const res = await dispatch('apiList', { url, objectKey: 'tree' });
if ( !pattern ) {
return res;
}
const out = res.filter(file => file.type === 'blob' && file.path.match(pattern));
return out;
},
};
export const mutations = {
setScopes(state, scopes) {
state.scopes = scopes;
},
setRepos(state, repos) {
state.repos = repos;
setCache(repos);
},
};