project runners (#2547)

* feat: add feature flag

* feat(runners): pro runners ui

* feat(runner): api for project level

* fix(ui): key of list

* feat(be): add mocks for project runners

* feat: pro alert

* feat(pro): upgrade button

* feat(pro): upgrade button

* feat(pro): icon and color

* feat(pro): change color
This commit is contained in:
Denis Gukov 2024-11-17 05:47:04 -05:00 committed by GitHub
parent cd0755f5a9
commit 4996728daa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 183 additions and 54 deletions

51
api/projects/runners.go Normal file
View File

@ -0,0 +1,51 @@
package projects
import (
"github.com/gorilla/context"
"github.com/semaphoreui/semaphore/api/helpers"
"github.com/semaphoreui/semaphore/db"
"net/http"
)
func GetRunners(w http.ResponseWriter, r *http.Request) {
project := context.Get(r, "project").(db.Project)
runners, err := helpers.Store(r).GetRunners(project.ID)
if err != nil {
panic(err)
}
var result = make([]db.Runner, 0)
for _, runner := range runners {
result = append(result, runner)
}
helpers.WriteJSON(w, http.StatusOK, result)
}
func AddRunner(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}
func RunnerMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r)
})
}
func GetRunner(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}
func UpdateRunner(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}
func DeleteRunner(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}
func SetRunnerActive(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}

View File

@ -12,13 +12,13 @@ import (
"github.com/semaphoreui/semaphore/api/runners" "github.com/semaphoreui/semaphore/api/runners"
"github.com/gorilla/mux"
"github.com/semaphoreui/semaphore/api/helpers" "github.com/semaphoreui/semaphore/api/helpers"
"github.com/semaphoreui/semaphore/api/projects" "github.com/semaphoreui/semaphore/api/projects"
"github.com/semaphoreui/semaphore/api/sockets" "github.com/semaphoreui/semaphore/api/sockets"
"github.com/semaphoreui/semaphore/api/tasks" "github.com/semaphoreui/semaphore/api/tasks"
"github.com/semaphoreui/semaphore/db" "github.com/semaphoreui/semaphore/db"
"github.com/semaphoreui/semaphore/util" "github.com/semaphoreui/semaphore/util"
"github.com/gorilla/mux"
) )
var startTime = time.Now().UTC() var startTime = time.Now().UTC()
@ -222,6 +222,16 @@ func Route() *mux.Router {
projectUserAPI.Path("/integrations").HandlerFunc(projects.AddIntegration).Methods("POST") projectUserAPI.Path("/integrations").HandlerFunc(projects.AddIntegration).Methods("POST")
projectUserAPI.Path("/backup").HandlerFunc(projects.GetBackup).Methods("GET", "HEAD") projectUserAPI.Path("/backup").HandlerFunc(projects.GetBackup).Methods("GET", "HEAD")
projectUserAPI.Path("/runners").HandlerFunc(projects.GetRunners).Methods("GET", "HEAD")
projectUserAPI.Path("/runners").HandlerFunc(projects.AddRunner).Methods("POST")
projectRunnersAPI := projectUserAPI.PathPrefix("/runners").Subrouter()
projectRunnersAPI.Use(globalRunnerMiddleware)
projectRunnersAPI.Path("/{runner_id}").HandlerFunc(projects.GetRunner).Methods("GET", "HEAD")
projectRunnersAPI.Path("/{runner_id}").HandlerFunc(projects.UpdateRunner).Methods("PUT", "POST")
projectRunnersAPI.Path("/{runner_id}/active").HandlerFunc(projects.SetRunnerActive).Methods("POST")
projectRunnersAPI.Path("/{runner_id}").HandlerFunc(projects.DeleteRunner).Methods("DELETE")
// //
// Updating and deleting project // Updating and deleting project
projectAdminAPI := authenticatedAPI.Path("/project/{project_id}").Subrouter() projectAdminAPI := authenticatedAPI.Path("/project/{project_id}").Subrouter()
@ -475,6 +485,10 @@ func getSystemInfo(w http.ResponseWriter, r *http.Request) {
"ansible": util.AnsibleVersion(), "ansible": util.AnsibleVersion(),
"web_host": host, "web_host": host,
"use_remote_runner": util.Config.UseRemoteRunner, "use_remote_runner": util.Config.UseRemoteRunner,
"premium_features": map[string]bool{
"project_runners": false,
},
} }
helpers.WriteJSON(w, http.StatusOK, body) helpers.WriteJSON(w, http.StatusOK, body)

View File

@ -9,14 +9,6 @@ import (
"github.com/gorilla/context" "github.com/gorilla/context"
) )
//type minimalGlobalRunner struct {
// ID int `json:"id"`
// Name string `json:"name"`
// Active bool `json:"active"`
// Webhook string `db:"webhook" json:"webhook"`
// MaxParallelTasks int `db:"max_parallel_tasks" json:"max_parallel_tasks"`
//}
func getGlobalRunners(w http.ResponseWriter, r *http.Request) { func getGlobalRunners(w http.ResponseWriter, r *http.Request) {
runners, err := helpers.Store(r).GetGlobalRunners(false) runners, err := helpers.Store(r).GetGlobalRunners(false)

View File

@ -528,6 +528,7 @@
:isAdmin="(user || {}).admin" :isAdmin="(user || {}).admin"
:webHost="(systemInfo || {}).web_host" :webHost="(systemInfo || {}).web_host"
:version="(systemInfo || {version: ''}).version.split('-')[0]" :version="(systemInfo || {version: ''}).version.split('-')[0]"
:premiumFeatures="((systemInfo || {premium_features: {}}).premium_features)"
:user="user" :user="user"
></router-view> ></router-view>
</v-main> </v-main>

View File

@ -64,7 +64,7 @@
'rgba(200, 200, 200, 0.38)' : 'rgba(200, 200, 200, 0.38)' :
'rgba(0, 0, 0, 0.38)' 'rgba(0, 0, 0, 0.38)'
}"> }">
<legend style="padding: 0 3px;">{{ $t('Args') }}</legend> <legend style="padding: 0 3px;">{{ title || $t('Args') }}</legend>
<v-chip-group column style="margin-top: -4px;"> <v-chip-group column style="margin-top: -4px;">
<v-chip <v-chip
v-for="(v, i) in modifiedVars" v-for="(v, i) in modifiedVars"
@ -89,6 +89,7 @@
export default { export default {
props: { props: {
vars: Array, vars: Array,
title: String,
}, },
watch: { watch: {
vars(val) { vars(val) {

View File

@ -0,0 +1,42 @@
<template>
<v-tabs show-arrows class="pl-4">
<v-tab
v-if="projectType === ''"
key="history"
:to="`/project/${projectId}/history`"
>{{ $t('history') }}
</v-tab>
<v-tab key="activity" :to="`/project/${projectId}/activity`">{{ $t('activity') }}</v-tab>
<v-tab
v-if="canUpdateProject"
key="settings"
:to="`/project/${projectId}/settings`"
>{{ $t('settings') }}
</v-tab>
<v-tab key="runners" :to="`/project/${projectId}/runners`">
{{ $t('runners') }}
<!-- <v-chip small class="ml-1" color="purple" style="color: white">Pro</v-chip> -->
<v-icon class="ml-1" large color="hsl(348deg, 86%, 61%)">mdi-professional-hexagon</v-icon>
</v-tab>
</v-tabs>
</template>
<script>
import PermissionsCheck from '@/components/PermissionsCheck';
export default {
mixins: [PermissionsCheck],
props: {
projectId: Number,
projectType: String,
canUpdateProject: Boolean,
},
data() {
return {
id: null,
};
},
};
</script>

View File

@ -59,7 +59,7 @@ export default {
}, },
getItemsUrl() { getItemsUrl() {
return '/api/projects/restore'; return '/api/project/restore';
}, },
}, },
}; };

View File

@ -274,6 +274,7 @@
<ArgsPicker <ArgsPicker
:vars="args" :vars="args"
@change="setArgs" @change="setArgs"
title="CLI args"
/> />
<v-checkbox <v-checkbox

View File

@ -301,7 +301,7 @@ export default {
taskLocation: 'Location', taskLocation: 'Location',
empty: 'Empty', empty: 'Empty',
noValues: 'No values', noValues: 'No values',
addArg: 'Add Arg', addArg: 'Add arg',
status_success: 'Success', status_success: 'Success',
status_failed: 'Failed', status_failed: 'Failed',

View File

@ -44,6 +44,10 @@ const routes = [
path: '/project/:projectId/activity', path: '/project/:projectId/activity',
component: Activity, component: Activity,
}, },
{
path: '/project/:projectId/runners',
component: Runners,
},
{ {
path: '/project/:projectId/schedule', path: '/project/:projectId/schedule',
component: Schedule, component: Schedule,

View File

@ -1,5 +1,19 @@
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform"> <template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
<div v-if="items != null"> <div v-if="items != null">
<v-toolbar flat >
<v-app-bar-nav-icon @click="showDrawer()"></v-app-bar-nav-icon>
<v-toolbar-title>
{{ $t('dashboard2') }}
</v-toolbar-title>
</v-toolbar>
<DashboardMenu
:project-id="projectId"
project-type=""
:can-update-project="can(USER_PERMISSIONS.updateProject)"
/>
<EditDialog <EditDialog
v-model="editDialog" v-model="editDialog"
:save-button-text="itemId === 'new' ? $t('create') : $t('save')" :save-button-text="itemId === 'new' ? $t('create') : $t('save')"
@ -101,7 +115,7 @@
@yes="deleteItem(itemId)" @yes="deleteItem(itemId)"
/> />
<v-toolbar flat> <v-toolbar flat v-if="!projectId">
<v-btn <v-btn
icon icon
class="mr-4" class="mr-4"
@ -109,6 +123,7 @@
> >
<v-icon>mdi-arrow-left</v-icon> <v-icon>mdi-arrow-left</v-icon>
</v-btn> </v-btn>
<v-toolbar-title>{{ $t('runners') }}</v-toolbar-title> <v-toolbar-title>{{ $t('runners') }}</v-toolbar-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn <v-btn
@ -118,6 +133,21 @@
</v-btn> </v-btn>
</v-toolbar> </v-toolbar>
<v-alert
v-if="!premiumFeatures.project_runners"
type="info"
text
color="hsl(348deg, 86%, 61%)"
style="border-radius: 0;"
>
Project Runners available only in <b>PRO</b> version.
<v-btn
class="ml-2"
color="hsl(348deg, 86%, 61%)"
href="https://semaphoreui.com/pro"
>Upgrade</v-btn>
</v-alert>
<v-data-table <v-data-table
:headers="headers" :headers="headers"
:items="items" :items="items"
@ -169,11 +199,13 @@ import ItemListPageBase from '@/components/ItemListPageBase';
import EditDialog from '@/components/EditDialog.vue'; import EditDialog from '@/components/EditDialog.vue';
import RunnerForm from '@/components/RunnerForm.vue'; import RunnerForm from '@/components/RunnerForm.vue';
import axios from 'axios'; import axios from 'axios';
import DashboardMenu from '@/components/DashboardMenu.vue';
export default { export default {
mixins: [ItemListPageBase], mixins: [ItemListPageBase],
components: { components: {
DashboardMenu,
RunnerForm, RunnerForm,
YesNoDialog, YesNoDialog,
EditDialog, EditDialog,
@ -182,6 +214,8 @@ export default {
props: { props: {
webHost: String, webHost: String,
version: String, version: String,
projectId: Number,
premiumFeatures: Object,
}, },
computed: { computed: {
@ -269,10 +303,18 @@ semaphore runner --no-config`;
}, },
getItemsUrl() { getItemsUrl() {
if (this.projectId) {
return `/api/project/${this.projectId}/runners`;
}
return '/api/runners'; return '/api/runners';
}, },
getSingleItemUrl() { getSingleItemUrl() {
if (this.projectId) {
return `/api/project/${this.projectId}/runners/${this.itemId}`;
}
return `/api/runners/${this.itemId}`; return `/api/runners/${this.itemId}`;
}, },

View File

@ -5,21 +5,11 @@
<v-toolbar-title>{{ $t('dashboard') }}</v-toolbar-title> <v-toolbar-title>{{ $t('dashboard') }}</v-toolbar-title>
</v-toolbar> </v-toolbar>
<v-tabs show-arrows class="pl-4"> <DashboardMenu
<v-tab :project-id="projectId"
v-if="projectType === ''" project-type=""
key="history" :can-update-project="can(USER_PERMISSIONS.updateProject)"
:to="`/project/${projectId}/history`" />
>{{ $t('history') }}</v-tab>
<v-tab key="activity" :to="`/project/${projectId}/activity`">{{ $t('activity') }}</v-tab>
<v-tab
v-if="can(USER_PERMISSIONS.updateProject)"
key="settings"
:to="`/project/${projectId}/settings`"
>
{{ $t('settings') }}
</v-tab>
</v-tabs>
<v-data-table <v-data-table
:headers="headers" :headers="headers"
@ -35,8 +25,10 @@
</template> </template>
<script> <script>
import ItemListPageBase from '@/components/ItemListPageBase'; import ItemListPageBase from '@/components/ItemListPageBase';
import DashboardMenu from '@/components/DashboardMenu.vue';
export default { export default {
components: { DashboardMenu },
mixins: [ItemListPageBase], mixins: [ItemListPageBase],

View File

@ -1,26 +1,17 @@
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform"> <template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
<div v-if="items != null"> <div v-if="items != null">
<v-toolbar flat > <v-toolbar flat>
<v-app-bar-nav-icon @click="showDrawer()"></v-app-bar-nav-icon> <v-app-bar-nav-icon @click="showDrawer()"></v-app-bar-nav-icon>
<v-toolbar-title> <v-toolbar-title>
{{ $t('dashboard2') }} {{ $t('dashboard2') }}
</v-toolbar-title> </v-toolbar-title>
</v-toolbar> </v-toolbar>
<v-tabs show-arrows class="pl-4"> <DashboardMenu
<v-tab :project-id="projectId"
v-if="projectType === ''" project-type=""
key="history" :can-update-project="can(USER_PERMISSIONS.updateProject)"
:to="`/project/${projectId}/history`" />
>{{ $t('history') }}</v-tab>
<v-tab key="activity" :to="`/project/${projectId}/activity`">{{ $t('activity') }}</v-tab>
<v-tab
v-if="can(USER_PERMISSIONS.updateProject)"
key="settings"
:to="`/project/${projectId}/settings`"
>{{ $t('settings') }}
</v-tab>
</v-tabs>
<v-data-table <v-data-table
:headers="headers" :headers="headers"
@ -102,6 +93,7 @@ import TaskLink from '@/components/TaskLink.vue';
import socket from '@/socket'; import socket from '@/socket';
import { TEMPLATE_TYPE_ICONS } from '@/lib/constants'; import { TEMPLATE_TYPE_ICONS } from '@/lib/constants';
import AppsMixin from '@/components/AppsMixin'; import AppsMixin from '@/components/AppsMixin';
import DashboardMenu from '@/components/DashboardMenu.vue';
export default { export default {
mixins: [ItemListPageBase, AppsMixin], mixins: [ItemListPageBase, AppsMixin],
@ -110,7 +102,7 @@ export default {
return { TEMPLATE_TYPE_ICONS }; return { TEMPLATE_TYPE_ICONS };
}, },
components: { TaskStatus, TaskLink }, components: { DashboardMenu, TaskStatus, TaskLink },
watch: { watch: {
async projectId() { async projectId() {

View File

@ -12,15 +12,11 @@
<v-toolbar-title>{{ $t('dashboard') }}</v-toolbar-title> <v-toolbar-title>{{ $t('dashboard') }}</v-toolbar-title>
</v-toolbar> </v-toolbar>
<v-tabs show-arrows class="pl-4"> <DashboardMenu
<v-tab :project-id="projectId"
v-if="projectType === ''" project-type=""
key="history" :can-update-project="true"
:to="`/project/${projectId}/history`" />
>{{ $t('history') }}</v-tab>
<v-tab key="activity" :to="`/project/${projectId}/activity`">{{ $t('activity') }}</v-tab>
<v-tab key="settings" :to="`/project/${projectId}/settings`">{{ $t('settings') }}</v-tab>
</v-tabs>
<div class="project-settings-form"> <div class="project-settings-form">
<div style="height: 300px;"> <div style="height: 300px;">
@ -107,9 +103,10 @@ import { getErrorMessage } from '@/lib/error';
import axios from 'axios'; import axios from 'axios';
import YesNoDialog from '@/components/YesNoDialog.vue'; import YesNoDialog from '@/components/YesNoDialog.vue';
import delay from '@/lib/delay'; import delay from '@/lib/delay';
import DashboardMenu from '@/components/DashboardMenu.vue';
export default { export default {
components: { YesNoDialog, ProjectForm }, components: { DashboardMenu, YesNoDialog, ProjectForm },
props: { props: {
projectId: Number, projectId: Number,
projectType: String, projectType: String,