mirror of https://github.com/rancher/dashboard.git
591 lines
15 KiB
Vue
591 lines
15 KiB
Vue
<script lang="ts">
|
|
import { defineComponent, inject } from 'vue';
|
|
import SortableTable from '@shell/components/SortableTable/index.vue';
|
|
import RadioButton from '@components/Form/Radio/RadioButton.vue';
|
|
import debounce from 'lodash/debounce';
|
|
import { isArray } from '@shell/utils/array';
|
|
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
|
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
|
import { GitUtils, Commit } from '@shell/utils/git';
|
|
import type { DebouncedFunc } from 'lodash';
|
|
|
|
interface commit {
|
|
[key: string]: any,
|
|
sha: string,
|
|
}
|
|
interface Data {
|
|
hasError: {
|
|
repo: boolean,
|
|
branch: boolean,
|
|
commits: boolean,
|
|
branches?: unknown,
|
|
},
|
|
|
|
repos: object[],
|
|
branches: object[],
|
|
commits: commit[],
|
|
|
|
selectedAccOrOrg: string | undefined,
|
|
selectedRepo: object | undefined,
|
|
selectedBranch: object | undefined,
|
|
selectedCommit: Commit | null,
|
|
}
|
|
|
|
interface CommitHeader {
|
|
name: string;
|
|
label: string;
|
|
width?: number;
|
|
formatter?: string;
|
|
formatterOpts?: { [key: string]: any };
|
|
value?: string | string[];
|
|
sort?: string | string[];
|
|
defaultSort?: boolean;
|
|
}
|
|
|
|
interface NonReactiveProps {
|
|
onSearchRepo: DebouncedFunc<(param: any) => Promise<any>> | undefined;
|
|
onSearchBranch: DebouncedFunc<(param: any) => Promise<any>> | undefined;
|
|
debounceTime: number;
|
|
}
|
|
|
|
const debounceTime = 1_000;
|
|
|
|
const provideProps: NonReactiveProps = {
|
|
onSearchRepo: undefined,
|
|
onSearchBranch: undefined,
|
|
debounceTime,
|
|
};
|
|
|
|
export default defineComponent({
|
|
emits: ['change'],
|
|
|
|
components: {
|
|
LabeledSelect,
|
|
SortableTable,
|
|
RadioButton,
|
|
LabeledInput,
|
|
},
|
|
|
|
props: {
|
|
value: {
|
|
type: Object,
|
|
default: null
|
|
},
|
|
|
|
type: {
|
|
type: String,
|
|
default: null
|
|
}
|
|
},
|
|
|
|
setup() {
|
|
const onSearchRepo = inject('onSearchRepo', provideProps.onSearchRepo);
|
|
const onSearchBranch = inject('onSearchBranch', provideProps.onSearchBranch);
|
|
const debounceTime = inject('debounceTime', provideProps.debounceTime);
|
|
|
|
return {
|
|
onSearchRepo,
|
|
onSearchBranch,
|
|
debounceTime,
|
|
};
|
|
},
|
|
|
|
data(): Data {
|
|
return {
|
|
hasError: {
|
|
repo: false,
|
|
branch: false,
|
|
commits: false,
|
|
},
|
|
|
|
repos: [],
|
|
branches: [],
|
|
commits: [] as commit[],
|
|
|
|
selectedAccOrOrg: undefined,
|
|
selectedRepo: undefined,
|
|
selectedBranch: undefined,
|
|
selectedCommit: null,
|
|
};
|
|
},
|
|
|
|
created() {
|
|
this.onSearchRepo = this.searchForResult(this.searchRepo);
|
|
this.onSearchBranch = this.searchForResult(this.searchBranch);
|
|
},
|
|
|
|
watch: {
|
|
value: {
|
|
async handler(neu, old) {
|
|
if (JSON.stringify(neu) === JSON.stringify(old)) {
|
|
return;
|
|
}
|
|
if (neu?.type !== old?.type) {
|
|
this.reset();
|
|
await this.loadSourceCache(neu.selectedAccOrOrg, neu.selectedRepo, neu.selectedBranch, neu.selectedCommit);
|
|
}
|
|
},
|
|
immediate: true,
|
|
deep: true,
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
commitHeaders(): CommitHeader[] {
|
|
return [
|
|
{
|
|
name: 'index',
|
|
label: this.t(`gitPicker.${ this.type }.tableHeaders.choose.label`),
|
|
width: 60,
|
|
},
|
|
{
|
|
name: 'sha',
|
|
label: this.t(`gitPicker.${ this.type }.tableHeaders.sha.label`),
|
|
width: 90,
|
|
formatter: 'Link',
|
|
formatterOpts: { urlKey: 'htmlUrl' },
|
|
value: 'sha'
|
|
},
|
|
{
|
|
name: 'author',
|
|
label: this.t(`gitPicker.${ this.type }.tableHeaders.author.label`),
|
|
width: 190,
|
|
value: 'author.login',
|
|
sort: 'author.login',
|
|
},
|
|
{
|
|
name: 'message',
|
|
label: this.t(`gitPicker.${ this.type }.tableHeaders.message.label`),
|
|
value: 'message',
|
|
sort: 'message',
|
|
},
|
|
{
|
|
name: 'date',
|
|
width: 220,
|
|
label: this.t(`gitPicker.${ this.type }.tableHeaders.date.label`),
|
|
value: 'date',
|
|
sort: ['date:desc'],
|
|
formatter: 'Date',
|
|
defaultSort: true,
|
|
},
|
|
];
|
|
},
|
|
|
|
preparedRepos(): unknown {
|
|
return this.normalizeArray(this.repos, (item: any) => ({ id: item.id, name: item.name }));
|
|
},
|
|
|
|
preparedBranches(): unknown {
|
|
return this.normalizeArray(this.branches, (item: any) => ({ id: item.id, name: item.name }));
|
|
},
|
|
|
|
preparedCommits(): Commit[] {
|
|
return this.normalizeArray(this.commits, (c) => GitUtils[this.type].normalize.commit(c));
|
|
},
|
|
selectedCommitId(): string | undefined {
|
|
return this.selectedCommit?.commitId;
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
debounceRequest(callback: (param: any) => Promise<any>, timeout = debounceTime) {
|
|
return debounce(async(param) => await callback(param), timeout);
|
|
},
|
|
|
|
searchForResult(callback: (query: string) => Promise<any>) {
|
|
return this.debounceRequest(async(query: string) => {
|
|
if (!query.length) {
|
|
return;
|
|
}
|
|
|
|
return await callback(query);
|
|
});
|
|
},
|
|
|
|
reset() {
|
|
this.repos = [];
|
|
this.selectedAccOrOrg = undefined;
|
|
this.selectedRepo = undefined;
|
|
this.selectedBranch = undefined;
|
|
this.selectedCommit = null;
|
|
|
|
this.communicateReset();
|
|
},
|
|
|
|
communicateReset() {
|
|
this.$emit('change', {
|
|
selectedAccOrOrg: this.selectedAccOrOrg,
|
|
repo: this.selectedRepo,
|
|
commit: this.selectedCommit,
|
|
});
|
|
},
|
|
|
|
async loadSourceCache(accOrOrg: string, repo: { name: string}, branch: { name: string}, commit: { sha: string}) {
|
|
this.selectedAccOrOrg = accOrOrg;
|
|
if (this.selectedAccOrOrg) {
|
|
await this.fetchRepos()
|
|
.then(() => {
|
|
if (this.repos.length && !this.hasError.repo) {
|
|
this.selectedRepo = repo;
|
|
|
|
return this.fetchBranches();
|
|
}
|
|
})
|
|
.then(() => {
|
|
if (this.branches.length && !this.hasError.branch) {
|
|
if (branch?.name) {
|
|
this.selectedBranch = branch;
|
|
|
|
return this.fetchCommits();
|
|
}
|
|
}
|
|
});
|
|
|
|
const selectedCommit = this.commits?.find((c: commit) => {
|
|
// Github has sha's
|
|
// Gitlab has id's as sha's
|
|
const sha = c.sha || c.id;
|
|
|
|
return sha === commit.sha;
|
|
});
|
|
|
|
if (selectedCommit) {
|
|
this.final(selectedCommit.sha || selectedCommit.id);
|
|
}
|
|
}
|
|
},
|
|
|
|
async fetchRepos() {
|
|
this.repos = [];
|
|
this.selectedRepo = undefined;
|
|
this.selectedBranch = undefined;
|
|
this.selectedCommit = null;
|
|
|
|
this.communicateReset();
|
|
|
|
if (this.selectedAccOrOrg?.length) {
|
|
try {
|
|
const res = await this.$store.dispatch(`${ this.type }/fetchRecentRepos`, { username: this.selectedAccOrOrg });
|
|
|
|
this.repos = res;
|
|
|
|
this.hasError.repo = false;
|
|
} catch (error) {
|
|
this.hasError.repo = true;
|
|
}
|
|
}
|
|
},
|
|
|
|
async fetchBranches() {
|
|
this.selectedBranch = undefined;
|
|
this.selectedCommit = null;
|
|
|
|
this.communicateReset();
|
|
|
|
try {
|
|
const res = await this.$store.dispatch(`${ this.type }/fetchBranches`, { repo: this.selectedRepo, username: this.selectedAccOrOrg });
|
|
|
|
this.branches = res;
|
|
this.hasError.branch = false;
|
|
} catch (error) {
|
|
this.hasError.branch = true;
|
|
}
|
|
},
|
|
|
|
async fetchCommits() {
|
|
this.selectedCommit = null;
|
|
|
|
this.communicateReset();
|
|
|
|
try {
|
|
const res = await this.$store.dispatch(`${ this.type }/fetchCommits`, {
|
|
repo: this.selectedRepo,
|
|
username: this.selectedAccOrOrg,
|
|
branch: this.selectedBranch,
|
|
});
|
|
|
|
this.commits = res as commit[];
|
|
|
|
this.hasError.branch = false;
|
|
} catch (error) {
|
|
this.hasError.commits = true;
|
|
}
|
|
},
|
|
|
|
normalizeArray(elem: any, normalize: (v: any) => object) {
|
|
const arr = isArray(elem) ? elem : [elem];
|
|
|
|
return arr.map((item: any) => normalize(item));
|
|
},
|
|
|
|
final(commitId: string) {
|
|
this.selectedCommit = this.preparedCommits.find((c: { commitId?: string }) => c.commitId === commitId) || null;
|
|
|
|
if (this.selectedAccOrOrg && this.selectedRepo && this.selectedCommit?.commitId) {
|
|
this.$emit('change', {
|
|
selectedAccOrOrg: this.selectedAccOrOrg,
|
|
repo: this.selectedRepo,
|
|
branch: this.selectedBranch,
|
|
commit: this.selectedCommit.commitId,
|
|
sourceData: {
|
|
repos: this.repos,
|
|
branches: this.branches,
|
|
commits: this.commits
|
|
}
|
|
|
|
});
|
|
}
|
|
},
|
|
|
|
async searchRepo(query: string) {
|
|
if (query.length) {
|
|
// Check if the result is already in the fetched list.
|
|
const resultInCurrentState = this.repos.filter((r: any) => r.name.startsWith(query));
|
|
|
|
if (!resultInCurrentState.length) {
|
|
// Search for specific repo under the username
|
|
const res = await this.$store.dispatch(`${ this.type }/search`, {
|
|
repo: { id: query, name: query },
|
|
username: this.selectedAccOrOrg
|
|
});
|
|
|
|
this.hasError.branches = !!res.hasError;
|
|
|
|
if (!res.hasError) {
|
|
if (res.length >= 1) {
|
|
this.repos = res;
|
|
}
|
|
}
|
|
} else {
|
|
return resultInCurrentState;
|
|
}
|
|
}
|
|
},
|
|
|
|
async searchBranch(query: string) {
|
|
const res = await this.$store.dispatch(`${ this.type }/search`, {
|
|
repo: this.selectedRepo,
|
|
branch: { name: query },
|
|
username: this.selectedAccOrOrg,
|
|
});
|
|
|
|
this.hasError.branch = !!res.hasError;
|
|
|
|
if (!this.hasError) {
|
|
this.branches = res;
|
|
}
|
|
},
|
|
|
|
status(value: boolean) {
|
|
return value ? 'error' : undefined;
|
|
},
|
|
|
|
reposRules() {
|
|
return this.hasError.repo ? this.t(`gitPicker.${ this.type }.errors.noAccount`) : null;
|
|
},
|
|
|
|
branchesRules() {
|
|
return this.hasError.branch ? this.t(`gitPicker.${ this.type }.errors.noBranch`) : null;
|
|
},
|
|
|
|
/**
|
|
* Show the page where the commit is, if any
|
|
*/
|
|
onCommitsTableMounted() {
|
|
const commitId = this.$route.query?.commit;
|
|
|
|
if (commitId) {
|
|
const table = this.$refs.commitsTable as unknown as {
|
|
getPageByRow: (id: string | (string | null)[], callback: (commit: any) => any) => any;
|
|
setPage: (page: any) => void;
|
|
};
|
|
const page = table?.getPageByRow(commitId, ({ commitId }: commit) => commitId);
|
|
|
|
table?.setPage(page);
|
|
}
|
|
},
|
|
selectReduction(e: unknown) {
|
|
return e;
|
|
}
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="picker">
|
|
<div class="row">
|
|
<div class="spacer">
|
|
<LabeledInput
|
|
v-model:value="selectedAccOrOrg"
|
|
data-testid="git_picker-username-or-org"
|
|
:tooltip="t(`gitPicker.${ type }.username.tooltip`)"
|
|
:label="t(`gitPicker.${ type }.username.inputLabel`)"
|
|
:required="true"
|
|
:rules="[reposRules]"
|
|
:delay="debounceTime"
|
|
:status="status(hasError.repo)"
|
|
@update:value="fetchRepos"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-if="repos.length && !hasError.repo"
|
|
class="spacer"
|
|
>
|
|
<LabeledSelect
|
|
v-model:value="selectedRepo"
|
|
:required="true"
|
|
:label="t(`gitPicker.${ type }.repo.inputLabel`)"
|
|
:options="preparedRepos"
|
|
:clearable="true"
|
|
:searchable="true"
|
|
:reduce="selectReduction"
|
|
:rules="[reposRules]"
|
|
:status="status(hasError.repo)"
|
|
:option-label="'name'"
|
|
@search="onSearchRepo"
|
|
@update:value="fetchBranches"
|
|
/>
|
|
</div>
|
|
<!-- Deals with Branches -->
|
|
<div
|
|
v-if="selectedRepo"
|
|
class="spacer"
|
|
>
|
|
<LabeledSelect
|
|
v-model:value="selectedBranch"
|
|
:required="true"
|
|
:label="t(`gitPicker.${ type }.branch.inputLabel`)"
|
|
:options="preparedBranches"
|
|
:clearable="false"
|
|
:reduce="selectReduction"
|
|
:searchable="true"
|
|
:rules="[branchesRules]"
|
|
:status="status(hasError.branch)"
|
|
:option-label="'name'"
|
|
@search="onSearchBranch"
|
|
@update:value="fetchCommits"
|
|
/>
|
|
</div>
|
|
<!-- Deals with Commits, display & allow to pick from it -->
|
|
<div
|
|
v-if="selectedBranch && preparedCommits.length"
|
|
class="commits-table mt-20"
|
|
>
|
|
<SortableTable
|
|
ref="commitsTable"
|
|
:rows="preparedCommits"
|
|
:headers="commitHeaders"
|
|
mode="view"
|
|
key-field="sha"
|
|
:search="true"
|
|
:paging="true"
|
|
:table-actions="false"
|
|
:row-actions="false"
|
|
:rows-per-page="10"
|
|
@hook:mounted="onCommitsTableMounted"
|
|
>
|
|
<template #cell:index="{row}">
|
|
<RadioButton
|
|
:value="selectedCommitId"
|
|
:val="row.commitId"
|
|
@update:value="final($event)"
|
|
/>
|
|
</template>
|
|
|
|
<template #cell:author="{row}">
|
|
<div class="sortable-table-avatar">
|
|
<template v-if="row.author">
|
|
<img
|
|
:src="row.author.avatarUrl"
|
|
alt=""
|
|
>
|
|
<a
|
|
:href="row.author.htmlUrl"
|
|
target="_blank"
|
|
rel="nofollow noopener noreferrer"
|
|
>
|
|
{{ row.author.name }}
|
|
</a>
|
|
</template>
|
|
<template v-else>
|
|
{{ t(`gitPicker.${ type }.tableHeaders.author.unknown`) }}
|
|
</template>
|
|
</div>
|
|
</template>
|
|
</SortableTable>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.picker {
|
|
.row {
|
|
display: flex;
|
|
flex-direction: column;
|
|
margin: 6px 0;
|
|
}
|
|
|
|
img {
|
|
height: 30px;
|
|
margin-right: 1rem;
|
|
|
|
.labeled-input {
|
|
width: 100%;
|
|
}
|
|
}
|
|
|
|
.sortable-table-avatar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-start;
|
|
|
|
img {
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: var(--border-radius);
|
|
}
|
|
}
|
|
|
|
.spacer {
|
|
max-width: 700px;
|
|
}
|
|
|
|
.commits-table {
|
|
margin: 0 1px;
|
|
max-width: 1400px;
|
|
}
|
|
|
|
.d-center {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.selected img {
|
|
width: auto;
|
|
max-height: 23px;
|
|
margin-right: 0.5rem;
|
|
}
|
|
|
|
.v-select .dropdown li {
|
|
border-bottom: 1px solid rgba(112, 128, 144, 0.1);
|
|
}
|
|
|
|
.v-select .dropdown li:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.v-select .dropdown li a {
|
|
padding: 10px 20px;
|
|
width: 100%;
|
|
font-size: 1.25em;
|
|
color: #3c3c3c;
|
|
}
|
|
|
|
.v-select .dropdown-menu .active > a {
|
|
color: #fff;
|
|
}
|
|
}
|
|
</style>
|