Skip to content

Commit 5a838bc

Browse files
authored
Merge branch 'github-owner' of 'https://github.com/evamillan/grimoirelab-core'
Merges #100 Closes #100
2 parents c1f3e83 + adb595e commit 5a838bc

File tree

9 files changed

+432
-132
lines changed

9 files changed

+432
-132
lines changed

src/grimoirelab/core/datasources/api.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,10 @@ def create(self, request, *args, **kwargs):
332332
job_max_retries = request.data["scheduler"].get("job_max_retries", job_max_retries)
333333

334334
task_args = {"uri": request.data["uri"]}
335+
336+
if "backend_args" in request.data:
337+
task_args = request.data["backend_args"]
338+
335339
task = schedule_task(
336340
"eventizer",
337341
task_args,

ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"build-storybook": "storybook build"
1818
},
1919
"dependencies": {
20+
"@octokit/rest": "^22.0.0",
2021
"axios": "^1.12.0",
2122
"js-cookie": "^3.0.5",
2223
"pinia": "^3.0.3",

ui/src/components/RepositoryTable.vue

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@
3232
</div>
3333
</div>
3434
</template>
35+
<template #[`item.data-table-select`]="{ internalItem, isSelected, toggleSelect }">
36+
<v-checkbox-btn
37+
:model-value="isSelected(internalItem)"
38+
:indeterminate="isRowIndeterminate(internalItem, isSelected)"
39+
color="primary"
40+
@update:model-value="selectRow(internalItem, isSelected, toggleSelect)"
41+
></v-checkbox-btn>
42+
</template>
3543
<template #[`item.url`]="{ item }">
3644
{{ item.url }}
3745
<v-chip v-if="item.fork" class="ml-2" color="primary" density="comfortable" size="small">
@@ -91,6 +99,11 @@
9199
</v-data-table-virtual>
92100
</template>
93101
<script>
102+
const enabledColumns = {
103+
issue: 'has_issues',
104+
pull_request: 'has_pull_requests'
105+
}
106+
94107
export default {
95108
name: 'RepositoryTable',
96109
emits: ['update:selected'],
@@ -116,9 +129,9 @@ export default {
116129
{ title: 'Pull Requests', value: 'pull_request' }
117130
],
118131
selectedColumns: {
119-
commit: true,
120-
issue: true,
121-
pull_request: true
132+
commit: false,
133+
issue: false,
134+
pull_request: false
122135
},
123136
selected: [],
124137
filters: {
@@ -164,9 +177,9 @@ export default {
164177
acc.push({
165178
...item,
166179
form: {
167-
commit: true,
168-
pull_request: item.has_pull_requests,
169-
issue: item.has_issues
180+
commit: false,
181+
pull_request: false,
182+
issue: false
170183
}
171184
})
172185
}
@@ -180,7 +193,30 @@ export default {
180193
areSomeSelected(column) {
181194
return (
182195
this.items.some((item) => item.form[column] === true) &&
183-
!this.items.every((item) => item.form[column] === true)
196+
!this.items.every(
197+
(item) =>
198+
item.form[column] === true || (enabledColumns[column] && !item[enabledColumns[column]])
199+
)
200+
)
201+
},
202+
selectRow(item, isSelected, toggleSelect) {
203+
const value = !isSelected(item)
204+
Object.keys(item.value.form).forEach((column) => {
205+
if (!enabledColumns[column] || item.value[enabledColumns[column]]) {
206+
item.value.form[column] = value
207+
}
208+
})
209+
toggleSelect(item)
210+
},
211+
isRowIndeterminate(item, isSelected) {
212+
return (
213+
isSelected(item) &&
214+
Object.values(item.value.form).some((value) => value === true) &&
215+
!Object.keys(item.value.form).every(
216+
(column) =>
217+
item.value.form[column] === true ||
218+
(enabledColumns[column] && !item.value[enabledColumns[column]])
219+
)
184220
)
185221
}
186222
},

ui/src/views/Ecosystem/ViewManager.vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export default {
1111
ProjectView: defineAsyncComponent(() => import('../Project/DetailView.vue')),
1212
RepoView: defineAsyncComponent(() => import('../Repo/DetailView.vue')),
1313
NewRepoView: defineAsyncComponent(() => import('../Repo/NewRepo.vue')),
14-
SBoMView: defineAsyncComponent(() => import('../Repo/LoadSbom.vue'))
14+
BulkCreateView: defineAsyncComponent(() => import('../Repo/BulkCreate.vue'))
1515
},
1616
props: {
1717
ecosystem: {
@@ -46,7 +46,9 @@ export default {
4646
} else if (this.create && this.create === 'repo') {
4747
return 'NewRepoView'
4848
} else if (this.create && this.create === 'sbom') {
49-
return 'SBoMView'
49+
return 'BulkCreateView'
50+
} else if (this.create && this.create === 'github') {
51+
return 'BulkCreateView'
5052
} else if (this.repo) {
5153
return 'RepoView'
5254
} else {

ui/src/views/Repo/BulkCreate.vue

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<template>
2+
<v-container>
3+
<component :is="currentComponent" @update:repos="repos = $event"></component>
4+
<router-view @update:repos="repos = $event"> </router-view>
5+
<div v-if="repos.length > 0">
6+
<repository-table :repositories="repos" @update:selected="form.selected = $event">
7+
</repository-table>
8+
<p class="text-subtitle-2 mt-6 mb-4">Schedule</p>
9+
<div class="mb-6">
10+
<interval-selector v-model="form.interval"></interval-selector>
11+
</div>
12+
<v-alert
13+
v-model="alert.isOpen"
14+
:text="alert.text"
15+
:icon="alert.icon"
16+
:color="alert.color"
17+
density="compact"
18+
class="mb-6"
19+
/>
20+
<v-btn :loading="loading" color="primary" @click="createTasks"> Schedule selected </v-btn>
21+
</div>
22+
</v-container>
23+
</template>
24+
<script>
25+
import IntervalSelector from '@/components/IntervalSelector.vue'
26+
import RepositoryTable from '@/components/RepositoryTable.vue'
27+
import { defineAsyncComponent } from 'vue'
28+
import { API } from '@/services/api'
29+
import { getTaskArgs } from '@/utils/datasources'
30+
31+
export default {
32+
components: {
33+
GitHubView: defineAsyncComponent(() => import('./GitHub.vue')),
34+
SBoMView: defineAsyncComponent(() => import('./LoadSbom.vue')),
35+
IntervalSelector,
36+
RepositoryTable
37+
},
38+
data() {
39+
return {
40+
errorMessage: null,
41+
loading: false,
42+
repos: [],
43+
form: {
44+
interval: '604800',
45+
selected: []
46+
},
47+
alert: {
48+
isOpen: false,
49+
text: '',
50+
color: 'error',
51+
icon: 'mdi-warning'
52+
}
53+
}
54+
},
55+
computed: {
56+
project() {
57+
return this.$route.query?.project
58+
},
59+
ecosystem() {
60+
return this.$route.query?.ecosystem
61+
},
62+
currentComponent() {
63+
if (this.$route.query?.create === 'github') {
64+
return 'GitHubView'
65+
} else if (this.$route.query?.create === 'sbom') {
66+
return 'SBoMView'
67+
}
68+
return null
69+
}
70+
},
71+
methods: {
72+
async createTasks() {
73+
if (this.form.selected.length === 0) {
74+
Object.assign(this.alert, {
75+
isOpen: true,
76+
color: 'error',
77+
text: 'Select at least one URL and one category (commits, issues and/or pull requests) to retrieve.',
78+
icon: 'mdi-alert-outline'
79+
})
80+
return
81+
}
82+
this.loading = true
83+
84+
Promise.allSettled(
85+
this.form.selected.map((task) => {
86+
const { datasource_type, category, uri, backend_args } = getTaskArgs(
87+
task.datasource,
88+
task.category,
89+
task.url
90+
)
91+
return API.repository.create(this.ecosystem, this.project, {
92+
datasource_type,
93+
category,
94+
uri,
95+
backend_args,
96+
scheduler: {
97+
job_interval: this.form.interval,
98+
job_max_retries: 3
99+
}
100+
})
101+
})
102+
)
103+
.then((responses) => {
104+
const errors = responses
105+
.filter((res) => res.status === 'rejected')
106+
.map((error) => {
107+
if (error.reason.response.data && typeof error.reason.response.data === 'object') {
108+
return Object.values(error.reason.response.data).toString()
109+
} else {
110+
return error.reason.message
111+
}
112+
})
113+
if (errors.length > 0) {
114+
Object.assign(this.alert, {
115+
isOpen: true,
116+
color: 'error',
117+
text: errors,
118+
icon: 'mdi-alert-outline'
119+
})
120+
} else {
121+
this.$router.push({
122+
name: 'ecosystems',
123+
query: { ecosystem: this.ecosystem, project: this.project }
124+
})
125+
}
126+
})
127+
.finally(() => {
128+
this.loading = false
129+
})
130+
}
131+
}
132+
}
133+
</script>
134+
<style lang="scss" scoped>
135+
:deep(.v-form) {
136+
max-width: 600px;
137+
}
138+
139+
:deep(.v-table) {
140+
background-color: transparent;
141+
142+
.v-table__wrapper {
143+
background-color: rgb(var(--v-theme-surface));
144+
border: thin solid rgba(0, 0, 0, 0.08);
145+
border-radius: 4px;
146+
max-height: 55vh;
147+
}
148+
}
149+
</style>

ui/src/views/Repo/GitHub.vue

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<template>
2+
<div>
3+
<h1 class="text-h6 my-4">GitHub</h1>
4+
<div v-if="!hasLoaded">
5+
<p class="mb-2">Load a list of repositories from a GitHub organization or user.</p>
6+
<v-form class="my-6" @submit.prevent>
7+
<v-text-field
8+
v-model="owner"
9+
color="primary"
10+
label="GitHub organization or user"
11+
density="compact"
12+
variant="outlined"
13+
@keyup.enter.prevent="fetchRepos(owner)"
14+
>
15+
<template #append>
16+
<v-btn :loading="loading" color="primary" @click="fetchRepos(owner)"> Load </v-btn>
17+
</template>
18+
</v-text-field>
19+
</v-form>
20+
</div>
21+
<p v-else class="mb-4">
22+
Found <span class="font-weight-medium">{{ repos.length }}</span>
23+
{{ repos.length === 1 ? 'repository' : 'repositories' }} for
24+
<span class="font-weight-medium">{{ owner }}</span
25+
>.
26+
<v-btn
27+
color="primary"
28+
class="text-body-2"
29+
density="comfortable"
30+
size="small"
31+
variant="text"
32+
@click="reloadForm"
33+
>
34+
<v-icon start>mdi-refresh</v-icon>
35+
New search
36+
</v-btn>
37+
</p>
38+
</div>
39+
</template>
40+
<script>
41+
import { Octokit } from '@octokit/rest'
42+
43+
export default {
44+
emits: ['update:repos'],
45+
data() {
46+
return {
47+
errorMessage: null,
48+
hasLoaded: false,
49+
loading: false,
50+
owner: '',
51+
repos: []
52+
}
53+
},
54+
methods: {
55+
async fetchRepos(owner) {
56+
if (!owner) return
57+
this.loading = true
58+
59+
const octokit = new Octokit()
60+
try {
61+
const response = await octokit.paginate(octokit.rest.repos.listForUser, {
62+
username: owner,
63+
per_page: 100
64+
})
65+
this.repos = response.map((repo) => ({
66+
datasource: 'github',
67+
name: repo.name,
68+
url: repo.html_url,
69+
fork: repo.fork,
70+
has_issues: repo.has_issues,
71+
has_pull_requests: true,
72+
archived: repo.archived
73+
}))
74+
this.$emit('update:repos', this.repos)
75+
} catch (error) {
76+
this.errorMessage = error
77+
} finally {
78+
this.loading = false
79+
this.hasLoaded = true
80+
}
81+
},
82+
reloadForm() {
83+
this.$emit('update:repos', [])
84+
this.owner = ''
85+
this.hasLoaded = false
86+
this.repos = []
87+
}
88+
}
89+
}
90+
</script>

0 commit comments

Comments
 (0)