Persistent deployment data (#6852)

* Persists Github deployment data
* Moves GitHub Deployment card to own component
* Fixes/Tweaks
- Add indicator to app detail commits list to show deployed commit
- Add l10n
- hide github description field if there's no description (phantom icon)
- add typing for app env var
- Fix application of app env var (add/remove as appropriate)

---------

Co-authored-by: Richard Cox <richard.cox@suse.com>
This commit is contained in:
Sorin 2023-02-10 15:14:20 +01:00 committed by GitHub
parent 93381d9cf3
commit 00e389d6da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 681 additions and 131 deletions

View File

@ -0,0 +1,150 @@
<script>
import day from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
export default {
props: {
gitSource: { default: null, type: Object },
commitPosition: {
default: null,
type: Object
},
gitDeployment: {
default: null,
type: Object
},
},
methods: {
formatDate(date, from) {
day.extend(relativeTime);
return from ? day(date).fromNow() : day(date).format('DD MMM YYYY');
},
},
};
</script>
<template>
<div class="repo-info">
<div class="repo-info-owner">
<img
:src="gitSource.owner.avatar_url"
alt=""
>
<div>
<a
ref="nofollow"
target="_blank"
:href="gitSource.owner.html_url"
>{{ gitSource.owner.login }}</a>
<span>/</span>
<a
ref="nofollow"
target="_blank"
:href="gitSource.html_url"
>{{ gitSource.name }}</a>
</div>
</div>
<div
v-if="gitDeployment.deployedCommit"
class="repo-info-revision"
>
<span>
<i class="icon icon-fw icon-commit" />
{{ gitDeployment.deployedCommit.short }}
</span>
<span
v-if="commitPosition"
class="masthead-state badge-state"
>
<i class="icon icon-fw icon-commit" />
{{ commitPosition.text }}
</span>
</div>
<div
v-if="gitSource.description"
class="repo-info-description"
>
<i class="icon icon-fw icon-comment" />
<p>
{{ gitSource.description }}
</p>
</div>
<ul>
<li>
<span>{{ t('epinio.applications.detail.deployment.details.gitHub.created') }}</span>: {{ formatDate(gitSource.created_at) }}
</li>
<li>
<span>{{ t('epinio.applications.detail.deployment.details.gitHub.updated') }}</span>: {{ formatDate(gitSource.updated_at, true) }}
</li>
</ul>
</div>
</template>
<style lang="scss" scoped>
.application-card {
margin-top: 0 !important;
}
.repo-info {
display: grid;
grid-auto-columns: minmax(0, 1fr);
grid-gap: 20px;
font-size: 14px;
&-owner {
display: flex;
align-self: center;
a {
font-size: 16px !important;
}
img {
margin-right: 8px;
align-self: center;
width: 20px;
border-radius: 5%;
}
span {
opacity: 0.5;
}
}
&-description, &-revision{
display: flex;
align-items: center;
align-self: center;
i {
opacity: 0.8;
}
span {
display: flex;
align-self: center;
}
}
&-revision {
justify-content: space-between;
}
ul {
margin: 0;
padding: 0;
list-style: none;
display: flex;
justify-content: space-between;
li {
font-size: 14px;
opacity: 0.5;
span {
color: var(--default-text);
}
}
}
}
</style>

View File

@ -1,15 +1,20 @@
<script lang="ts"> <script lang="ts">
import day from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import Vue, { PropType } from 'vue'; import Vue, { PropType } from 'vue';
import Application from '../models/applications'; import Application from '../models/applications';
import SimpleBox from '@shell/components/SimpleBox.vue'; import SimpleBox from '@shell/components/SimpleBox.vue';
import ConsumptionGauge from '@shell/components/ConsumptionGauge.vue'; import ConsumptionGauge from '@shell/components/ConsumptionGauge.vue';
import { EPINIO_PRODUCT_NAME, EPINIO_TYPES } from '../types'; import { APPLICATION_ENV_VAR, EPINIO_APP_ENV_VAR_GITHUB, EPINIO_PRODUCT_NAME, EPINIO_TYPES } from '../types';
import ResourceTable from '@shell/components/ResourceTable.vue'; import ResourceTable from '@shell/components/ResourceTable.vue';
import PlusMinus from '@shell/components/form/PlusMinus.vue'; import PlusMinus from '@shell/components/form/PlusMinus.vue';
import { epinioExceptionToErrorsArray } from '../utils/errors'; import { epinioExceptionToErrorsArray } from '../utils/errors';
import ApplicationCard from '@shell/components/cards/ApplicationCard.vue'; import ApplicationCard from '@shell/components/cards/ApplicationCard.vue';
import Tabbed from '@shell/components/Tabbed/index.vue'; import Tabbed from '@shell/components/Tabbed/index.vue';
import Tab from '@shell/components/Tabbed/Tab.vue'; import Tab from '@shell/components/Tabbed/Tab.vue';
import SortableTable from '@shell/components/SortableTable/index.vue';
import AppGitHubDeployment from '../components/application/AppGitHubDeployment.vue';
import Link from '@shell/components/formatter/Link.vue';
interface Data { interface Data {
} }
@ -19,11 +24,14 @@ export default Vue.extend<Data, any, any, any>({
components: { components: {
SimpleBox, SimpleBox,
ConsumptionGauge, ConsumptionGauge,
SortableTable,
ResourceTable, ResourceTable,
PlusMinus, PlusMinus,
ApplicationCard, ApplicationCard,
AppGitHubDeployment,
Tabbed, Tabbed,
Tab, Tab,
Link
}, },
props: { props: {
value: { value: {
@ -42,8 +50,8 @@ export default Vue.extend<Data, any, any, any>({
fetch() { fetch() {
this.$store.dispatch(`epinio/findAll`, { type: EPINIO_TYPES.SERVICE_INSTANCE }); this.$store.dispatch(`epinio/findAll`, { type: EPINIO_TYPES.SERVICE_INSTANCE });
this.$store.dispatch(`epinio/findAll`, { type: EPINIO_TYPES.CONFIGURATION }); this.$store.dispatch(`epinio/findAll`, { type: EPINIO_TYPES.CONFIGURATION });
this.fetchRepoDetails();
}, },
data() { data() {
const appInstanceSchema = this.$store.getters[`${ EPINIO_PRODUCT_NAME }/schemaFor`](EPINIO_TYPES.APP_INSTANCE); const appInstanceSchema = this.$store.getters[`${ EPINIO_PRODUCT_NAME }/schemaFor`](EPINIO_TYPES.APP_INSTANCE);
const servicesSchema = this.$store.getters[`${ EPINIO_PRODUCT_NAME }/schemaFor`](EPINIO_TYPES.SERVICE_INSTANCE); const servicesSchema = this.$store.getters[`${ EPINIO_PRODUCT_NAME }/schemaFor`](EPINIO_TYPES.SERVICE_INSTANCE);
@ -52,7 +60,12 @@ export default Vue.extend<Data, any, any, any>({
const configsHeaders: [] = this.$store.getters['type-map/headersFor'](configsSchema); const configsHeaders: [] = this.$store.getters['type-map/headersFor'](configsSchema);
return { return {
saving: false, saving: false,
gitSource: null,
gitDeployment: {
deployedCommit: null,
commitsArray: null,
},
appInstance: { appInstance: {
schema: appInstanceSchema, schema: appInstanceSchema,
headers: this.$store.getters['type-map/headersFor'](appInstanceSchema), headers: this.$store.getters['type-map/headersFor'](appInstanceSchema),
@ -64,10 +77,37 @@ export default Vue.extend<Data, any, any, any>({
configs: { configs: {
schema: configsSchema, schema: configsSchema,
headers: configsHeaders.filter((h: any) => !['namespace', 'boundApps', 'service'].includes(h.name)), headers: configsHeaders.filter((h: any) => !['namespace', 'boundApps', 'service'].includes(h.name)),
} },
commitsTableHeaders: [{
name: 'sha',
label: this.t('githubPicker.tableHeaders.sha.label'),
width: 100,
},
{
name: 'author',
label: this.t('githubPicker.tableHeaders.author.label'),
width: 190,
value: 'author.login',
sort: 'author.login',
},
{
name: 'message',
label: this.t('githubPicker.tableHeaders.message.label'),
value: 'message',
sort: 'message',
},
{
name: 'date',
width: 220,
label: this.t('githubPicker.tableHeaders.date.label'),
value: 'date',
sort: ['date:desc'],
formatter: 'Date',
defaultSort: true,
},
]
}; };
}, },
methods: { methods: {
async updateInstances(newInstances: number) { async updateInstances(newInstances: number) {
this.$set(this, 'saving', true); this.$set(this, 'saving', true);
@ -84,19 +124,112 @@ export default Vue.extend<Data, any, any, any>({
const matchGithub = str.match('^(https|git)(:\/\/|@)([^\/:]+)[\/:]([^\/:]+)\/(.+)(.git)*$'); const matchGithub = str.match('^(https|git)(:\/\/|@)([^\/:]+)[\/:]([^\/:]+)\/(.+)(.git)*$');
return `${ matchGithub?.[4] }/${ matchGithub?.[5] }`; return `${ matchGithub?.[4] }/${ matchGithub?.[5] }`;
},
async fetchRepoDetails() {
const envs = this.value?.envDetails;
if (envs[APPLICATION_ENV_VAR] ) {
const { usernameOrOrg, repo } = JSON.parse(envs[APPLICATION_ENV_VAR]) as EPINIO_APP_ENV_VAR_GITHUB;
const res = await this.$store.dispatch('github/fetchRepoDetails', { username: usernameOrOrg, repo });
const {
// eslint-disable-next-line camelcase
owner, description, created_at, updated_at, html_url, name
} = res;
this.gitSource = {
owner,
description,
created_at,
updated_at,
html_url,
name
};
const commit = this.value.sourceInfo?.details.filter((ele: { label: string; }) => ele.label === 'Revision')[0]?.value;
if (commit) {
this.gitDeployment.deployedCommit = {
short: commit?.slice(0, 7),
long: commit
};
}
}
await this.fetchCommits();
},
async fetchCommits() {
const envs = this.value?.envDetails;
if (!envs[APPLICATION_ENV_VAR]) {
return;
}
const { usernameOrOrg, repo, branch } = JSON.parse(envs[APPLICATION_ENV_VAR]);
this.gitDeployment.commitsArray = await this.$store.dispatch('github/fetchCommits', {
username: usernameOrOrg, repo, branch
});
},
formatDate(date: string, from: boolean) {
day.extend(relativeTime);
return from ? day(date).fromNow() : day(date).format('DD MMM YYYY');
} }
}, },
computed: { computed: {
prepareCommitArray() {
if (this.gitDeployment.commitsArray.length) {
return this.gitDeployment.commitsArray.reduce((acc: any, cur: any) => {
acc.push({
message: cur.commit.message,
html_url: cur.html_url,
sha: cur.sha.slice(0, 7),
commitId: cur?.sha,
author: cur.author,
isChecked: false,
date: cur?.commit.committer.date
});
return acc;
}, []);
}
return [];
},
sourceIcon(): string { sourceIcon(): string {
return this.value.sourceInfo?.icon || 'icon-epinio'; return this.value.sourceInfo?.icon || 'icon-epinio';
},
commitPosition() {
if (!this.gitDeployment?.commitsArray && !this.gitDeployment.deployedCommit) {
return;
}
let idx = null;
if (this.gitDeployment.commitsArray) {
this.gitDeployment.commitsArray.map((ele: { sha: any; }, i: number) => {
if (ele.sha === this.gitDeployment.deployedCommit.long) {
idx = i - 1;
}
});
}
if (!idx) {
return idx;
}
return {
text: ( idx - 1) >= 0 ? `${ idx } ${ this.t('epinio.applications.gitSource.behindCommits') }` : this.t('epinio.applications.gitSource.latestCommit'),
position: idx
};
} }
} }
}); });
</script> </script>
<template> <template>
<div> <div class="content">
<div class="application-details"> <div class="application-details">
<ApplicationCard> <ApplicationCard>
<!-- Icon slot --> <!-- Icon slot -->
@ -159,90 +292,196 @@ export default Vue.extend<Data, any, any, any>({
v-if="value.deployment" v-if="value.deployment"
class="deployment" class="deployment"
> >
<div class="simple-box-row app-instances"> <!-- Source information -->
<SimpleBox> <Tabbed>
<ConsumptionGauge <Tab
:resource-name="t('epinio.applications.detail.deployment.instances')" label-key="epinio.applications.detail.tables.overview"
:capacity="value.desiredInstances" name="overview"
:used="value.readyInstances" :weight="3"
:used-as-resource-name="true" >
:color-stops="{ 70: '--success', 30: '--warning', 0: '--error' }" <div class="simple-box-row app-instances">
/> <SimpleBox>
<div class="scale-instances"> <ConsumptionGauge
<PlusMinus :resource-name="t('epinio.applications.detail.deployment.instances')"
class="mt-15 mb-10" :capacity="value.desiredInstances"
:value="value.desiredInstances" :used="value.readyInstances"
:disabled="saving" :used-as-resource-name="true"
@minus="updateInstances(value.desiredInstances - 1)" :color-stops="{ 70: '--success', 30: '--warning', 0: '--error' }"
@plus="updateInstances(value.desiredInstances + 1)" />
/> <div class="scale-instances">
</div> <PlusMinus
</SimpleBox> class="mt-15 mb-10"
<!-- Source information --> :value="value.desiredInstances"
<SimpleBox v-if="value.sourceInfo"> :disabled="saving"
<div class="deployment__origin__row"> @minus="updateInstances(value.desiredInstances - 1)"
<h4>Deployment Details</h4> @plus="updateInstances(value.desiredInstances + 1)"
</div> />
<div class="deployment__origin__list"> </div>
<ul>
<li>
<h4>Origin</h4>
<span v-if="value.sourceInfo.label === 'Git'">
<i class="icon icon-fw icon-github" />
{{ value.sourceInfo.label }}
</span>
<span v-else>{{ value.sourceInfo.label }}</span>
</li>
<li <div class="deployment__origin__row">
v-for="d of value.sourceInfo.details" <hr class="mt-10 mb-10">
:key="d.label" <h4 class="mt-10 mb-10">
{{ t('epinio.applications.detail.deployment.metrics') }}
</h4>
<div
v-if="gitSource"
class="stats"
>
<div>
<h3>{{ t('tableHeaders.memory') }}</h3>
<ul>
<li> <span>Min: </span> {{ value.instanceMemory.min }}</li>
<li> <span>Max: </span>{{ value.instanceMemory.max }}</li>
<li><span>Avg: </span>{{ value.instanceMemory.avg }}</li>
</ul>
</div>
<div>
<h3>{{ t('tableHeaders.cpu') }}</h3>
<ul>
<li> <span>Min: </span> {{ value.instanceCpu.min }}</li>
<li> <span>Max: </span>{{ value.instanceCpu.max }}</li>
<li><span>Avg: </span>{{ value.instanceCpu.avg }}</li>
</ul>
</div>
</div>
<div
v-else
class="stats-table"
>
<table class="mt-15">
<thead>
<tr>
<th />
<th>Min</th>
<th>Max</th>
<th>Avg</th>
</tr>
</thead>
<tr>
<td>{{ t('tableHeaders.memory') }}</td>
<td>{{ value.instanceMemory.min }}</td>
<td>{{ value.instanceMemory.max }}</td>
<td>{{ value.instanceMemory.avg }}</td>
</tr>
<tr>
<td>{{ t('tableHeaders.cpu') }}</td>
<td>{{ value.instanceCpu.min }}</td>
<td>{{ value.instanceCpu.max }}</td>
<td>{{ value.instanceCpu.avg }}</td>
</tr>
</table>
</div>
</div>
</SimpleBox>
<SimpleBox v-if="value.sourceInfo">
<h4 class="mb-10">
{{ t('epinio.applications.detail.deployment.details.label') }}
</h4>
<div
v-if="gitSource"
class="repo-info"
> >
<h4>{{ d.label }}</h4> <AppGitHubDeployment
<span v-if="d.value && d.value.startsWith('http')"> :git-deployment="gitDeployment"
<a :git-source="gitSource"
:href="d.value" :commit-position="commitPosition"
target="_blank" />
>{{ formatURL(d.value) }}</a> </div>
</span> <hr class="mt-10 mb-10">
<span v-else>{{ d.value }}</span> <div class="deployment__origin__list">
</li> <ul>
<li>
<h4>{{ t('epinio.applications.detail.deployment.details.origin') }}</h4>
<span>{{ value.sourceInfo.label }}</span>
</li>
<li> <li
<h4>{{ t('epinio.applications.tableHeaders.deployedBy') }}</h4> v-for="d of value.sourceInfo.details"
<span> {{ value.deployment.username }}</span> :key="d.label"
</li> >
</ul> <h4>{{ d.label }}</h4>
<span v-if="d.value && d.value.startsWith('http')">
<a
:href="d.value"
target="_blank"
>{{ formatURL(d.value) }}</a>
</span>
<span v-else-if="gitSource && d.value && d.value.match(/^[a-f0-9]{40}$/)">
<a
:href="`${gitSource.html_url}/commit/${d.value}`"
target="_blank"
>{{ d.value }}</a>
</span>
<span v-else>{{ d.value }}</span>
</li>
<li>
<h4>{{ t('epinio.applications.tableHeaders.deployedBy') }}</h4>
<span> {{ value.deployment.username }}</span>
</li>
</ul>
</div>
</SimpleBox>
</div> </div>
</SimpleBox> </Tab>
<SimpleBox> <Tab
<div class="deployment__origin__row"> v-if="gitSource"
<h4>Application Metrics</h4> label-key="epinio.applications.detail.tables.githubCommits"
<table class="stats mt-15"> name="githubCommits"
<thead> :weight="2"
<tr> >
<th /> <SortableTable
<th>Min</th> v-if="gitDeployment.commitsArray"
<th>Max</th> :rows="prepareCommitArray"
<th>Avg</th> :headers="commitsTableHeaders"
</tr> mode="view"
</thead> key-field="sha"
<tr> :search="true"
<td>{{ t('tableHeaders.memory') }}</td> :paging="true"
<td>{{ value.instanceMemory.min }}</td> :table-actions="false"
<td>{{ value.instanceMemory.max }}</td> :row-actions="false"
<td>{{ value.instanceMemory.avg }}</td> :rows-per-page="10"
</tr> >
<tr> <template #cell:author="{row}">
<td>{{ t('tableHeaders.cpu') }}</td> <div class="sortable-table-avatar">
<td>{{ value.instanceCpu.min }}</td> <template v-if="row.author">
<td>{{ value.instanceCpu.max }}</td> <img
<td>{{ value.instanceCpu.avg }}</td> :src="row.author.avatar_url"
</tr> alt=""
</table> >
</div> <a
</SimpleBox> :href="row.author.html_url"
</div> target="_blank"
rel="nofollow noopener noreferrer"
>
{{ row.author.login }}
</a>
</template>
<template v-else>
{{ t('githubPicker.tableHeaders.author.unknown') }}
</template>
</div>
</template>
<template #cell:sha="{row}">
<div class="sortable-table-commit">
<Link
:row="row"
url-key="html_url"
:value="row.sha"
/>
<i
v-if="row.sha === gitDeployment.deployedCommit.short"
v-tooltip="t('epinio.applications.detail.deployment.details.gitHub.deployed')"
class="icon icon-fw icon-commit"
/>
</div>
</template>
</SortableTable>
</Tab>
</Tabbed>
</div> </div>
<h3 class="mt-20"> <h3 class="mt-20">
@ -295,6 +534,9 @@ export default Vue.extend<Data, any, any, any>({
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.content {
max-width: 1600px;
}
.simple-box-row { .simple-box-row {
display: grid; display: grid;
grid-auto-columns: minmax(0, 1fr); grid-auto-columns: minmax(0, 1fr);
@ -308,7 +550,6 @@ export default Vue.extend<Data, any, any, any>({
width: 100%; width: 100%;
ul { ul {
word-break: break-all; word-break: break-all;
padding-left: 20px;
} }
&:not(:last-of-type) { &:not(:last-of-type) {
margin-right: 20px; margin-right: 20px;
@ -332,28 +573,22 @@ export default Vue.extend<Data, any, any, any>({
tr { tr {
th { th {
text-align: left; text-align: left;
color: #c4c4c4; color: var(--muted);
font-weight: 300; font-weight: 300;
} }
} }
} }
} }
.deployment__origin__list {
ul {
margin: 20px 0;
padding: 0;
display: grid;
grid-template-columns: 1fr 1fr;
li { .scale-instances {
margin: 5px; display: flex;
list-style: none; align-items: center;
h4 {
color: #c4c4c4; .plus-minus {
font-weight: 300; width: 100%;
margin: 0; display: flex;
} align-items: center;
} justify-content: center;
} }
} }
} }
@ -394,22 +629,97 @@ export default Vue.extend<Data, any, any, any>({
} }
} }
.deployment { .stats-table {
.simple-box { display: flex;
width: 100%;
table {
width: 100%; width: 100%;
margin-bottom: 0;
} }
.app-instances { }
tr td {
min-width: 58px; .stats {
padding: 5px 0; display: grid;
font-size: 1.1rem; grid-template-columns: 1fr 1fr;
margin: 12px 0;
position: relative;
&::before {
content: "";
border-right: 1px solid var(--default);
position: absolute;
top: 0;
bottom: 0;
left: 50%;
width: 1px;
}
& > div:nth-child(2) {
display: flex;
flex-direction: column;
align-items: flex-end;
}
h3 {
font-size: 16px;
}
ul {
display: flex;
flex-direction: column;
gap: 8px;
margin: 0;
padding: 0;
li {
list-style: none;
font-size: 14px;
} }
.scale-instances { }
margin-top: 20px;
display: flex; // For the second div in stats, style the ul differently
justify-content: center; & > div:nth-child(2) ul {
align-items: flex-end;
}
}
.deployment__origin__list {
ul {
margin: 0;
padding: 0;
display: grid;
grid-template-columns: 1fr 1fr;
li {
margin: 5px;
list-style: none;
h4 {
color: var(--default-text);
font-weight: 300;
font-size: 14px;
margin: 0;
}
} }
} }
} }
.sortable-table {
&-avatar {
display: flex;
align-items: center;
justify-content: flex-start;
img {
width: 30px;
height: 30px;
border-radius: var(--border-radius);
margin-right: 10px;
}
}
&-commit {
display: flex;
}
}
</style> </style>

View File

@ -98,13 +98,26 @@ epinio:
label: Deployment label: Deployment
summary: Summary summary: Summary
instances: Instances instances: Instances
metrics: Metrics
memory: Memory memory: Memory
cpu: CPU cpu: CPU
details:
label: Deployment Details
origin: Origin
gitHub:
created: Created
updated: Updated
deployed: Deployed
tables: tables:
label: Resources label: Resources
instances: Instances instances: Instances
services: Services services: Services
configs: Configurations configs: Configurations
overview: Overview
githubCommits: Github commits
gitSource:
latestCommit: Latest commit deployed
behindCommits: Commits behind
create: create:
title: Application title: Application
titleSubText: Epinio titleSubText: Epinio

View File

@ -1,7 +1,7 @@
import Resource from '@shell/plugins/dashboard-store/resource-class'; import Resource from '@shell/plugins/dashboard-store/resource-class';
import Vue from 'vue';
import { APPLICATION_ACTION_STATE, APPLICATION_MANIFEST_SOURCE_TYPE, APPLICATION_SOURCE_TYPE, EPINIO_PRODUCT_NAME } from '../types'; import { APPLICATION_ACTION_STATE, APPLICATION_MANIFEST_SOURCE_TYPE, APPLICATION_SOURCE_TYPE, EPINIO_PRODUCT_NAME } from '../types';
import { epinioExceptionToErrorsArray } from '../utils/errors'; import { epinioExceptionToErrorsArray } from '../utils/errors';
import Vue from 'vue';
export const APPLICATION_ACTION_TYPE = { export const APPLICATION_ACTION_TYPE = {
CREATE_NS: 'create_namespace', CREATE_NS: 'create_namespace',
@ -166,7 +166,8 @@ export default class ApplicationActionResource extends Resource {
kind: APPLICATION_MANIFEST_SOURCE_TYPE.GIT_HUB, kind: APPLICATION_MANIFEST_SOURCE_TYPE.GIT_HUB,
git: { git: {
revision: source.github.commit, revision: source.github.commit,
repository: source.github.url repository: source.github.url,
branch: source.github.branch
}, },
}; };
} }

View File

@ -1,10 +1,10 @@
import { APPLICATION_MANIFEST_SOURCE_TYPE, EPINIO_PRODUCT_NAME, EPINIO_TYPES } from '../types';
import { formatSi } from '@shell/utils/units';
import { classify } from '@shell/plugins/dashboard-store/classify'; import { classify } from '@shell/plugins/dashboard-store/classify';
import EpinioMetaResource from './epinio-namespaced-resource';
import { downloadFile } from '@shell/utils/download'; import { downloadFile } from '@shell/utils/download';
import { createEpinioRoute } from '../utils/custom-routing'; import { formatSi } from '@shell/utils/units';
import { epiniofy } from '../store/epinio-store/actions'; import { epiniofy } from '../store/epinio-store/actions';
import { APPLICATION_ACTION_STATE, APPLICATION_MANIFEST_SOURCE_TYPE, EPINIO_PRODUCT_NAME, EPINIO_TYPES } from '../types';
import { createEpinioRoute } from '../utils/custom-routing';
import EpinioMetaResource from './epinio-namespaced-resource';
// See https://github.com/epinio/epinio/blob/00684bc36780a37ab90091498e5c700337015a96/pkg/api/core/v1/models/app.go#L11 // See https://github.com/epinio/epinio/blob/00684bc36780a37ab90091498e5c700337015a96/pkg/api/core/v1/models/app.go#L11
const STATES = { const STATES = {
@ -206,6 +206,10 @@ export default class EpinioApplicationModel extends EpinioMetaResource {
return Object.keys(this.configuration?.environment || []).length; return Object.keys(this.configuration?.environment || []).length;
} }
get envDetails() {
return this.configuration?.environment;
}
get routeCount() { get routeCount() {
return this.configuration?.routes.length; return this.configuration?.routes.length;
} }
@ -258,9 +262,10 @@ export default class EpinioApplicationModel extends EpinioMetaResource {
value: this.origin.git.repository value: this.origin.git.repository
}, { }, {
label: 'Revision', label: 'Revision',
icon: 'icon-github', icon: 'icon-commit',
value: this.origin.git.revision value: this.origin.git.revision
}] },
]
}; };
case APPLICATION_MANIFEST_SOURCE_TYPE.CONTAINER: case APPLICATION_MANIFEST_SOURCE_TYPE.CONTAINER:
return { return {
@ -272,6 +277,20 @@ export default class EpinioApplicationModel extends EpinioMetaResource {
value: this.origin.Container || this.origin.container value: this.origin.Container || this.origin.container
}] }]
}; };
case APPLICATION_MANIFEST_SOURCE_TYPE.GIT_HUB:
return {
label: 'GitHub',
icon: 'icon-github',
details: [
appChart, {
label: 'Url',
value: this.origin.git.repository
}, {
label: 'Revision',
icon: 'icon-github',
value: this.origin.git.revision
}]
};
default: default:
return undefined; return undefined;
} }
@ -627,7 +646,7 @@ export default class EpinioApplicationModel extends EpinioMetaResource {
// 'deployed' status. Unfortunately we don't have that... so wait for ready === desired replica sets instead // 'deployed' status. Unfortunately we don't have that... so wait for ready === desired replica sets instead
const fresh = this.$getters['byId'](EPINIO_TYPES.APP, `${ this.meta.namespace }/${ this.meta.name }`); const fresh = this.$getters['byId'](EPINIO_TYPES.APP, `${ this.meta.namespace }/${ this.meta.name }`);
if (fresh.deployment?.readyreplicas === fresh.deployment?.desiredreplicas) { if (fresh.deployment?.readyreplicas === fresh.deployment?.desiredreplicas && fresh.deployment.state === APPLICATION_ACTION_STATE.SUCCESS) {
return true; return true;
} }
// This is an async fn, but we're in a sync fn. It might create a backlog if previous requests don't complete in time // This is an async fn, but we're in a sync fn. It might create a backlog if previous requests don't complete in time

View File

@ -0,0 +1,12 @@
import EpinioMetaResource from '~/pkg/epinio/models/epinio-namespaced-resource';
export default class GithubCommits extends EpinioMetaResource {
get availableActions() {
return [{
action: 'github-commits',
label: this.t('epinio.applications.actions.shell.label'),
icon: 'icon icon-fw icon-chevron-right',
enabled: true,
}];
}
}

View File

@ -4,7 +4,7 @@ import Application from '../../../../../models/applications';
import CreateEditView from '@shell/mixins/create-edit-view/impl'; import CreateEditView from '@shell/mixins/create-edit-view/impl';
import Loading from '@shell/components/Loading.vue'; import Loading from '@shell/components/Loading.vue';
import Wizard from '@shell/components/Wizard.vue'; import Wizard from '@shell/components/Wizard.vue';
import { EPINIO_TYPES } from '../../../../../types'; import { APPLICATION_ENV_VAR, APPLICATION_SOURCE_TYPE, EPINIO_APP_ENV_VAR_GITHUB, EPINIO_TYPES } from '../../../../../types';
import { _CREATE } from '@shell/config/query-params'; import { _CREATE } from '@shell/config/query-params';
import AppInfo, { EpinioAppInfo } from '../../../../../components/application/AppInfo.vue'; import AppInfo, { EpinioAppInfo } from '../../../../../components/application/AppInfo.vue';
import AppSource, { EpinioAppSource } from '../../../../../components/application/AppSource.vue'; import AppSource, { EpinioAppSource } from '../../../../../components/application/AppSource.vue';
@ -107,12 +107,29 @@ export default Vue.extend<Data, any, any, any>({
this.source = {}; this.source = {};
const { appChart, ...cleanChanges } = changes; const { appChart, ...cleanChanges } = changes;
this.value.configuration = this.value.configuration || {};
if (appChart) { if (appChart) {
// app chart actually belongs in config, so stick it in there // app chart actually belongs in config, so stick it in there
this.value.configuration = this.value.configuration || {};
this.set(this.value.configuration, { appchart: appChart }); this.set(this.value.configuration, { appchart: appChart });
} }
if (changes.type === APPLICATION_SOURCE_TYPE.GIT_HUB) {
this.value.configuration.environment = this.value.configuration.environment || {};
const githubEnvVar: EPINIO_APP_ENV_VAR_GITHUB = {
usernameOrOrg: changes.github.usernameOrOrg as string,
repo: changes.github.repo,
branch: changes.github.branch,
};
this.set(this.value.configuration.environment, {
...this.value.configuration.environment,
[APPLICATION_ENV_VAR]: JSON.stringify(githubEnvVar)
});
} else {
delete this.value.configuration?.environment?.[APPLICATION_ENV_VAR];
}
this.set(this.source, cleanChanges); this.set(this.source, cleanChanges);
}, },

View File

@ -50,6 +50,13 @@ export const APPLICATION_ACTION_STATE = {
PENDING: 'pending', PENDING: 'pending',
}; };
export const APPLICATION_ENV_VAR = 'EPINIO_APP_DATA';
export interface EPINIO_APP_ENV_VAR_GITHUB {
usernameOrOrg: string,
repo: string,
branch: string,
}
// -------------------------------------- // --------------------------------------
// Temporary code until models are typed // Temporary code until models are typed
interface EpinioMeta { interface EpinioMeta {

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
import Collapse from '@shell/components/Collapse.vue'; import Collapse from '@shell/components/Collapse.vue';
import { mount } from '@vue/test-utils';
describe('component: Collapse.vue', () => { describe('component: Collapse.vue', () => {
describe('closed', () => { describe('closed', () => {

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
import SimpleBox from '@shell/components/SimpleBox.vue'; import SimpleBox from '@shell/components/SimpleBox.vue';
import { mount } from '@vue/test-utils';
describe('component: SimpleBox.vue', () => { describe('component: SimpleBox.vue', () => {
const wrapper = mount(SimpleBox, { propsData: { title: 'Simple box title' } }); const wrapper = mount(SimpleBox, { propsData: { title: 'Simple box title' } });

View File

@ -53,7 +53,7 @@ export default {
width: 220, width: 220,
label: this.t('githubPicker.tableHeaders.date.label'), label: this.t('githubPicker.tableHeaders.date.label'),
value: 'date', value: 'date',
sort: 'date:desc', sort: ['date:desc'],
formatter: 'Date', formatter: 'Date',
defaultSort: true, defaultSort: true,
}, },

View File

@ -3,6 +3,13 @@ const GITHUB_BASE_API = 'https://api.github.com';
const fetchGithubAPI = async(endpoint) => { const fetchGithubAPI = async(endpoint) => {
const response = await fetch(`${ GITHUB_BASE_API }/${ endpoint }`); const response = await fetch(`${ GITHUB_BASE_API }/${ endpoint }`);
// If rate-limit is exceeded, we should wait until the rate limit is reset
if (response.status === 403) {
const resetTime = new Date(response.headers.get('X-RateLimit-Reset') * 1000);
throw new Error(`Rate limit exceeded. Try again at ${ resetTime }`);
}
if (!response.ok) { if (!response.ok) {
throw response; throw response;
} }
@ -10,6 +17,8 @@ const fetchGithubAPI = async(endpoint) => {
return await response.json(); return await response.json();
}; };
export const getters = {};
export const actions = { export const actions = {
async apiList(ctx, { async apiList(ctx, {
username, endpoint, repo, branch username, endpoint, repo, branch
@ -19,6 +28,9 @@ export const actions = {
case 'branches': { case 'branches': {
return await fetchGithubAPI(`repos/${ username }/${ repo }/branches?sort=updated&per_page=100&direction=desc`); return await fetchGithubAPI(`repos/${ username }/${ repo }/branches?sort=updated&per_page=100&direction=desc`);
} }
case 'repo': {
return await fetchGithubAPI(`repos/${ username }/${ repo }`);
}
case 'commits': { case 'commits': {
return await fetchGithubAPI(`repos/${ username }/${ repo }/commits?sha=${ branch }&sort=updated&per_page=100`); return await fetchGithubAPI(`repos/${ username }/${ repo }/commits?sha=${ branch }&sort=updated&per_page=100`);
} }
@ -51,6 +63,14 @@ export const actions = {
return res; return res;
}, },
async fetchRepoDetails({ commit, dispatch }, { username, repo } = {}) {
const res = await dispatch('apiList', {
username, endpoint: 'repo', repo
});
return res;
},
async fetchBranches({ commit, dispatch }, { repo, username }) { async fetchBranches({ commit, dispatch }, { repo, username }) {
const res = await dispatch('apiList', { const res = await dispatch('apiList', {
username, endpoint: 'branches', repo username, endpoint: 'branches', repo
@ -59,7 +79,8 @@ export const actions = {
return res; return res;
}, },
async fetchCommits({ commit, dispatch }, { repo, username, branch }) { async fetchCommits(ctx, { repo, username, branch }) {
const { dispatch } = ctx;
const res = await dispatch('apiList', { const res = await dispatch('apiList', {
username, endpoint: 'commits', repo, branch username, endpoint: 'commits', repo, branch
}); });