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; direction: ltr;
position: relative; position: relative;
margin: 0; margin: 0;
overflow: hidden;
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
&.overflow-hidden {
overflow: hidden;
}
} }
::-webkit-scrollbar { ::-webkit-scrollbar {

View File

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

View File

@ -1,11 +1,13 @@
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { addParam, parseLinkHeader } from '@/utils/url'; import { findBy } from '../utils/array';
import { addObjects, isArray } from '@/utils/array'; import { EXTENDED_SCOPES } from '@/store/github';
import { EXTENDED_SCOPES } from '@/store/auth';
const API_BASE = 'https://api.github.com/'; export const FILE_PATTERNS = {
'dockerfile': /^Dockerfile(\..*)?$/i,
'yaml': /^.*\.ya?ml?$/i,
};
export default { export default {
props: { props: {
@ -14,19 +16,20 @@ export default {
required: true, required: true,
}, },
// filter files displayed in dropdown - default to .yml and Dockerfile files // Filter files displayed in dropdown by keys in FILE_PATTERNS
filePattern: { filePattern: {
type: RegExp, type: String,
default: () => { default: null,
return new RegExp('\.ya?ml$|^Dockerfile(\..*)?', 'i');
}
}, },
preferredFile: {
type: String,
default: null,
}
}, },
data() { data() {
return { return {
scopes: [],
recentRepos: [],
repos: [], repos: [],
branches: [], branches: [],
files: [], files: [],
@ -42,8 +45,45 @@ export default {
}, },
computed: { computed: {
...mapState({
scopes: state => state.github.scopes,
recentRepos: state => state.github.repos,
}),
hasPrivate() { hasPrivate() {
return this.scopes.includes('repo'); 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: { methods: {
update() {
if ( this.selectedRepo && this.selectedBranch && this.selectedFile ) {
// Do something
}
},
selectRepo(repo) { selectRepo(repo) {
this.selectedFile = null;
this.selectedBranch = null; this.selectedBranch = null;
this.selectedRepo = repo; this.selectedRepo = repo;
this.$emit('selectedRepo', repo);
this.fetchBranches(repo); this.fetchBranches(repo);
this.update();
}, },
selectBranch(branch) { selectBranch(branch) {
this.selectedFile = null;
this.selectedBranch = branch; this.selectedBranch = branch;
this.$emit('selectedBranch', branch); this.$emit('selectedBranch', branch);
this.fetchFiles(this.selectedRepo, branch); this.fetchFiles(this.selectedRepo, branch);
@ -96,7 +144,8 @@ export default {
try { try {
loading(true); 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; this.repos = res;
} catch (err) { } catch (err) {
@ -108,12 +157,9 @@ export default {
async fetchRepos() { async fetchRepos() {
try { try {
if ( !this.recentRepos.length) { const res = await this.$store.dispatch('github/fetchRecentRepos');
const res = await this.apiList('/user/repos?sort=updated', { depaginate: false });
this.recentRepos = res; this.repos = res;
this.repos = res.slice();
}
} finally { } finally {
this.loadingRecentRepos = false; this.loadingRecentRepos = false;
} }
@ -122,69 +168,43 @@ export default {
async fetchBranches(repo) { async fetchBranches(repo) {
this.loadingBranches = true; this.loadingBranches = true;
const url = repo.branches_url.replace('{/branch}', ''); try {
const res = await this.$store.dispatch('github/fetchBranches', { repo: this.selectedRepo });
const res = await this.apiList(url); this.branches = res;
this.branches = res; if ( !this.selectedBranch ) {
this.loadingBranches = false; const master = findBy(this.branches, 'name', 'master');
if ( master ) {
this.selectBranch(master);
}
}
} finally {
this.loadingBranches = false;
}
}, },
async fetchFiles(repo, branch) { 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.loadingFiles = false; this.selectFile(file);
}, }
}
proxifyUrl(url) { } finally {
// Strip off absolute links to github API this.loadingFiles = false;
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="row">
<div class="col span-4"> <div class="col span-4">
<v-select <v-select
:placeholder="loadingRecentRepos ? 'Loading...' : 'Choose a Repository...'" :placeholder="repoPlaceholder"
:disabled="loadingRecentRepos" :disabled="loadingRecentRepos"
:options="repos" :options="repos"
label="full_name" label="full_name"
@ -240,7 +260,7 @@ export default {
<div class="col span-4"> <div class="col span-4">
<v-select <v-select
:disabled="!selectedRepo || loadingBranches" :disabled="!selectedRepo || loadingBranches"
placeholder="Choose branch" :placeholder="branchPlaceholder"
:options="branches" :options="branches"
label="name" label="name"
:value="selectedBranch" :value="selectedBranch"
@ -252,7 +272,7 @@ export default {
<div class="col span-4"> <div class="col span-4">
<v-select <v-select
:disabled="!selectedBranch" :disabled="!selectedBranch"
placeholder="Choose file" :placeholder="filePlaceholder"
:options="files" :options="files"
label="path" label="path"
:value="selectedFile" :value="selectedFile"

View File

@ -138,18 +138,18 @@ export default {
<div class="row"> <div class="row">
<div class="col span-12"> <div class="col span-12">
<label class="radio"> <label class="radio">
<input type="radio" value="forwardOne"> <input type="radio" value="forwardOne">
Forward to Service Forward to Service
</label> </label>
<label class="radio"> <label class="radio">
<input type="radio" value="forwardMany"> <input type="radio" value="forwardMany">
Forward to Multiple Services Forward to Multiple Services
</label> </label>
<label class="radio"> <label class="radio">
<input type="radio" value="redirect"> <input type="radio" value="redirect">
Redirect Redirect
</label> </label>
</div> </div>
</div> </div>
@ -205,15 +205,15 @@ export default {
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<label> <label>
<input v-model="shouldMirror" type="checkbox" /> <input v-model="shouldMirror" type="checkbox" />
Mirror Mirror
</label> </label>
<label class="ml-20"> <label class="ml-20">
<input v-model="shouldFault" type="checkbox" /> <input v-model="shouldFault" type="checkbox" />
Fault Fault
</label> </label>
</div> </div>
<div v-if="shouldMirror" class="row"> <div v-if="shouldMirror" class="row">
<div class="col span-12"> <div class="col span-12">

View File

@ -189,7 +189,12 @@ export default {
<div v-if="buildMode === 'github'" class="row"> <div v-if="buildMode === 'github'" class="row">
<div class="col span-12"> <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>
</div> </div>

View File

@ -52,7 +52,7 @@ export default {
<div class="text-center"> <div class="text-center">
<AsyncButton v-if="isEdit" key="edit" mode="edit" @click="save" /> <AsyncButton v-if="isEdit" key="edit" mode="edit" @click="save" />
<AsyncButton v-if="isCreate" key="create" mode="create" @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 Cancel
</button> </button>
</div> </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 desired = 0;
let current = 0; let current = 0;
let count = 0;
for ( const service of forThisApp ) { for ( const service of forThisApp ) {
const weights = service.weights; const weights = service.weights;
desired += weights.desired || 0; desired += weights.desired || 0;
current += weights.current || 0; current += weights.current || 0;
count++;
} }
desired = Math.max(1, desired); desired = Math.max(1, desired);
current = Math.max(1, current); current = Math.max(1, current);
return { desired, current }; return {
desired,
current,
count
};
}, },
desired() { desired() {
@ -67,7 +73,10 @@ export default {
<template> <template>
<div> <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 }}% {{ current }}%
</span> </span>
<div v-if="showDesired"> <div v-if="showDesired">

View File

@ -2,7 +2,7 @@ import { CONFIG_MAP, SECRET, RIO } from '@/config/types';
import { import {
STATE, NAME, NAMESPACE_NAME, AGE, STATE, NAME, NAMESPACE_NAME, AGE,
RIO_IMAGE, WEIGHT, SCALE, RIO_IMAGE, WEIGHT, SCALE,
KEYS, KEYS, ENDPOINTS,
TARGET, TARGET_KIND, TARGET, TARGET_KIND,
} from '@/config/table-headers'; } from '@/config/table-headers';
@ -58,6 +58,7 @@ export const FRIENDLY = {
STATE, STATE,
NAMESPACE_NAME, NAMESPACE_NAME,
RIO_IMAGE, RIO_IMAGE,
ENDPOINTS,
WEIGHT, WEIGHT,
SCALE, SCALE,
AGE, 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', label: 'State',
sort: ['stateSort', 'nameSort'], sort: ['stateSort', 'nameSort'],
value: 'stateDisplay', value: 'stateDisplay',
width: 90, width: 75,
default: 'unknown', default: 'unknown',
formatter: 'BadgeState', formatter: 'BadgeState',
}; };
@ -55,8 +55,8 @@ export const AGE = {
value: 'metadata.creationTimestamp', value: 'metadata.creationTimestamp',
sort: ['createdTs', 'nameSort'], sort: ['createdTs', 'nameSort'],
search: false, search: false,
width: 75,
formatter: 'LiveDate', formatter: 'LiveDate',
width: 75,
align: 'right' align: 'right'
}; };
@ -67,13 +67,22 @@ export const RIO_IMAGE = {
sort: ['imageDisplay', 'nameSort'], sort: ['imageDisplay', 'nameSort'],
}; };
export const ENDPOINTS = {
name: 'endpoint',
label: 'Endpoint',
value: 'status.endpoints',
formatter: 'Endpoints',
width: 60,
align: 'center',
};
export const SCALE = { export const SCALE = {
name: 'scale', name: 'scale',
label: 'Scale', label: 'Scale',
value: 'scales.desired', value: 'scales.desired',
sort: ['scales.desired', 'nameSort'], sort: ['scales.desired', 'nameSort'],
width: 100,
formatter: 'Scale', formatter: 'Scale',
width: 60,
align: 'center', align: 'center',
}; };
@ -81,9 +90,10 @@ export const WEIGHT = {
name: 'weight', name: 'weight',
label: 'Weight', label: 'Weight',
value: 'status.computedWeight', value: 'status.computedWeight',
width: 100, sort: 'status.computedWeight',
align: 'center',
formatter: 'Weight', formatter: 'Weight',
width: 60,
align: 'center',
}; };
export const SUCCESS = { export const SUCCESS = {

View File

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

View File

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

View File

@ -159,7 +159,7 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.login { .login {
overflow: hidden; overflow: hidden;
.row { .row {
align-items: center; align-items: center;
} }

View File

@ -6,6 +6,7 @@ import AsyncButton from '@/components/AsyncButton';
import { SETUP, STEP, _DELETE } from '@/config/query-params'; import { SETUP, STEP, _DELETE } from '@/config/query-params';
import { RANCHER } from '@/config/types'; import { RANCHER } from '@/config/types';
import { open, popupWindowOptions } from '@/utils/window'; import { open, popupWindowOptions } from '@/utils/window';
import { findBy, filterBy, addObject } from '@/utils/array';
export default { export default {
layout: 'plain', layout: 'plain',
@ -15,6 +16,15 @@ export default {
}, },
computed: { 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() { passwordSubmitDisabled() {
if ( this.useRandom ) { if ( this.useRandom ) {
return false; return false;
@ -38,6 +48,18 @@ export default {
return false; 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 }) { async asyncData({ route, req, store }) {
@ -56,12 +78,19 @@ export default {
opt: { url: '/v3/settings/telemetry-opt' } opt: { url: '/v3/settings/telemetry-opt' }
}); });
const githubConfig = await store.dispatch('rancher/find', { let githubConfig = await store.dispatch('rancher/find', {
type: RANCHER.AUTH_CONFIG, type: RANCHER.AUTH_CONFIG,
id: 'github', id: 'github',
opt: { url: '/v3/authConfigs/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 origin;
let serverUrl = serverUrlSetting.value; let serverUrl = serverUrlSetting.value;
@ -110,6 +139,8 @@ export default {
hostname: githubConfig.hostname || 'github.com', hostname: githubConfig.hostname || 'github.com',
tls: kind === 'public' || githubConfig.tls, tls: kind === 'public' || githubConfig.tls,
githubError: null, githubError: null,
principals
}; };
}, },
@ -222,6 +253,25 @@ export default {
description: 'Initial setup session', 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); buttonCb(true);
this.step = 4; this.step = 4;
this.$router.applyQuery({ [STEP]: this.step }); 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('/'); this.$router.replace('/');
}, },
}, },
@ -341,16 +403,11 @@ export default {
<div class="col span-6 offset-3"> <div class="col span-6 offset-3">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input v-model="telemetry" type="checkbox" disabled /> <input v-model="telemetry" type="checkbox" />
Allow collection of anonymous statistics (required during beta period). Allow collection of anonymous statistics
</label> </label>
<i v-tooltip="{content: telemetryTooltip, placement: 'right', trigger: 'click'}" class="icon icon-info" />
</div> </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>
</div> </div>
@ -452,7 +509,7 @@ export default {
<div class="row mt-20"> <div class="row mt-20">
<div class="col span-6 offset-3 text-center" style="font-size: 24pt"> <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 Skip
</button> </button>
<AsyncButton key="githubSubmit" mode="continue" :disabled="serverSubmitDisabled" @click="testGithub" /> <AsyncButton key="githubSubmit" mode="continue" :disabled="serverSubmitDisabled" @click="testGithub" />
@ -461,7 +518,58 @@ export default {
</div> </div>
<div v-if="step === 4"> <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> </div>
</form> </form>
</template> </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() { save() {
delete this.__rehydrate;
return (opt = {}) => { return (opt = {}) => {
delete this.__rehydrate;
if ( !opt.url ) { if ( !opt.url ) {
opt.url = this.linkFor('self'); 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 { parse as parseUrl, addParam, addParams } from '@/utils/url';
import { findBy, addObjects } from '@/utils/array'; import { findBy, addObjects } from '@/utils/array';
import { BACK_TO, SPA, AUTH_TEST, _FLAGGED } from '@/config/query-params'; import { BACK_TO, SPA, AUTH_TEST, _FLAGGED } from '@/config/query-params';
import { BASE_SCOPES } from '@/store/github';
const KEY = 'rc_nonce'; const KEY = 'rc_nonce';
const BASE_SCOPES = ['read:user', 'read:org', 'user:email'];
export const EXTENDED_SCOPES = ['repo'];
const ERR_NONCE = 'nonce'; const ERR_NONCE = 'nonce';
const ERR_CLIENT = 'client'; 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);
},
};