Add global settings (#40)

This commit is contained in:
Jonas Plum
2022-03-13 13:45:10 +01:00
committed by GitHub
parent 86daadc73d
commit 18a4dc54e7
30 changed files with 1297 additions and 255 deletions

View File

@@ -164,6 +164,7 @@ export default Vue.extend({
{ icon: "mdi-account-group", name: "Groups", to: "GroupList", role: "admin:group:write", tier: "enterprise" },
{ icon: "mdi-cogs", name: "User Data", to: "UserDataList", role: "admin:userdata:write" },
{ icon: "mdi-format-list-checks", name: "Jobs", to: "JobList", role: "admin:job:write" },
{ icon: "mdi-cog", name: "Settings", to: "Settings", role: "admin:settings:write" },
],
mini: true,
goto: "",

View File

@@ -33,6 +33,12 @@ export interface Artifact {
* @memberof Artifact
*/
'enrichments'?: { [key: string]: Enrichment; };
/**
*
* @type {string}
* @memberof Artifact
*/
'kind'?: string;
/**
*
* @type {string}
@@ -976,50 +982,81 @@ export interface RuleResponse {
* @interface Settings
*/
export interface Settings {
/**
*
* @type {Array<Type>}
* @memberof Settings
*/
'artifactKinds': Array<Type>;
/**
*
* @type {Array<Type>}
* @memberof Settings
*/
'artifactStates': Array<Type>;
/**
*
* @type {Array<string>}
* @memberof Settings
*/
'roles'?: Array<string>;
/**
*
* @type {Array<TicketTypeResponse>}
* @memberof Settings
*/
'ticketTypes': Array<TicketTypeResponse>;
/**
*
* @type {string}
* @memberof Settings
*/
'tier': SettingsTierEnum;
/**
*
* @type {string}
* @memberof Settings
*/
'timeformat': string;
}
/**
*
* @export
* @interface SettingsResponse
*/
export interface SettingsResponse {
/**
*
* @type {Array<Type>}
* @memberof SettingsResponse
*/
'artifactKinds': Array<Type>;
/**
*
* @type {Array<Type>}
* @memberof SettingsResponse
*/
'artifactStates': Array<Type>;
/**
*
* @type {Array<string>}
* @memberof SettingsResponse
*/
'roles'?: Array<string>;
/**
*
* @type {Array<TicketTypeResponse>}
* @memberof SettingsResponse
*/
'ticketTypes': Array<TicketTypeResponse>;
/**
*
* @type {string}
* @memberof Settings
* @memberof SettingsResponse
*/
'tier': SettingsResponseTierEnum;
/**
*
* @type {string}
* @memberof SettingsResponse
*/
'timeformat': string;
/**
*
* @type {string}
* @memberof SettingsResponse
*/
'version': string;
}
export const SettingsTierEnum = {
export const SettingsResponseTierEnum = {
Community: 'community',
Enterprise: 'enterprise'
} as const;
export type SettingsTierEnum = typeof SettingsTierEnum[keyof typeof SettingsTierEnum];
export type SettingsResponseTierEnum = typeof SettingsResponseTierEnum[keyof typeof SettingsResponseTierEnum];
/**
*
@@ -4319,6 +4356,42 @@ export const SettingsApiAxiosParamCreator = function (configuration?: Configurat
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary Save settings
* @param {Settings} settings Save settings
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
saveSettings: async (settings: Settings, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'settings' is not null or undefined
assertParamExists('saveSettings', 'settings', settings)
const localVarPath = `/settings`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(settings, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
@@ -4340,10 +4413,21 @@ export const SettingsApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getSettings(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Settings>> {
async getSettings(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SettingsResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getSettings(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary Save settings
* @param {Settings} settings Save settings
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async saveSettings(settings: Settings, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SettingsResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.saveSettings(settings, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};
@@ -4360,9 +4444,19 @@ export const SettingsApiFactory = function (configuration?: Configuration, baseP
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getSettings(options?: any): AxiosPromise<Settings> {
getSettings(options?: any): AxiosPromise<SettingsResponse> {
return localVarFp.getSettings(options).then((request) => request(axios, basePath));
},
/**
*
* @summary Save settings
* @param {Settings} settings Save settings
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
saveSettings(settings: Settings, options?: any): AxiosPromise<SettingsResponse> {
return localVarFp.saveSettings(settings, options).then((request) => request(axios, basePath));
},
};
};
@@ -4383,6 +4477,18 @@ export class SettingsApi extends BaseAPI {
public getSettings(options?: AxiosRequestConfig) {
return SettingsApiFp(this.configuration).getSettings(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Save settings
* @param {Settings} settings Save settings
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SettingsApi
*/
public saveSettings(settings: Settings, options?: AxiosRequestConfig) {
return SettingsApiFp(this.configuration).saveSettings(settings, options).then((request) => request(this.axios, this.basePath));
}
}

View File

@@ -9,6 +9,9 @@
<v-icon small class="mr-1" :color="statusColor">{{ statusIcon }}</v-icon>
<span :class="statusColor + '--text'">{{ artifact.status | capitalize }}</span>
<v-icon small class="mx-1" :color="kindColor">{{ kindIcon }}</v-icon>
<span :class="kindColor + '--text'">{{ artifact.kind | capitalize }}</span>
<v-spacer></v-spacer>
<v-icon small class="mr-1">mdi-information</v-icon>
<span class="mr-1">{{ artifact.enrichments ? lodash.size(artifact.enrichments) : 0 }}</span>
@@ -47,6 +50,24 @@ export default Vue.extend({
}
})
return color;
},
kindIcon: function () {
let icon = "mdi-help";
this.lodash.forEach(this.$store.state.settings.artifactKinds, (state: Type) => {
if (this.artifact.kind === state.id) {
icon = state.icon;
}
})
return icon;
},
kindColor: function () {
let color = TypeColorEnum.Info as TypeColorEnum;
this.lodash.forEach(this.$store.state.settings.artifactKinds, (state: Type) => {
if (this.artifact.kind === state.id && state.color) {
color = state.color;
}
})
return color;
}
},
methods: {

View File

@@ -26,6 +26,7 @@ import Group from "@/views/Group.vue";
import TicketType from '../views/TicketType.vue';
import TicketTypeList from "@/views/TicketTypeList.vue";
import TaskList from "@/views/TaskList.vue";
import Settings from "@/views/Settings.vue";
Vue.use(VueRouter);
@@ -226,6 +227,13 @@ const routes: Array<RouteConfig> = [
]
},
{
path: "/settings",
name: "Settings",
component: Settings,
meta: { title: "Settings" },
},
{
path: "/apidocs",
name: "API",

View File

@@ -1,7 +1,7 @@
import Vue from "vue";
import Vuex, {ActionContext} from "vuex";
import {API} from "@/services/api";
import {UserData, TicketList, Settings, UserResponse} from "@/client";
import {UserData, TicketList, Settings, UserResponse, SettingsResponse} from "@/client";
import {AxiosResponse} from "axios";
import {Alert} from "@/types/types";
import {templateStore} from "./modules/templates";
@@ -68,7 +68,7 @@ export default new Vuex.Store({
})
},
getSettings (context: ActionContext<any, any>) {
API.getSettings().then((response: AxiosResponse<Settings>) => {
API.getSettings().then((response: AxiosResponse<SettingsResponse>) => {
context.commit("setSettings", response.data);
context.dispatch("fetchCount");
})

View File

@@ -22,6 +22,23 @@
</v-list>
</v-menu>
&middot;
Kind:
<v-menu offset-y class="mr-2">
<template v-slot:activator="{ on, attrs }">
<span v-bind="attrs" v-on="on">
<v-icon small class="mr-1" :color="kindColor(artifact.kind)">{{ kindIcon(artifact.kind) }}</v-icon>
<span :class="kindColor(artifact.kind) + '--text'">{{ artifact.kind | capitalize }}</span>
</span>
</template>
<v-list>
<v-list-item dense link v-for="state in otherKinds" :key="state.id" @click="setKind(state.id)">
<v-list-item-title>
Set kind to <v-icon small>{{ kindIcon(state.id) }}</v-icon> {{ state.name }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
&middot;
Type:
<v-menu
:close-on-content-click="false"
@@ -148,6 +165,14 @@ export default Vue.extend({
return state.id !== this.artifact.status;
})
},
otherKinds: function (): Array<Type> {
return this.lodash.filter(this.$store.state.settings.artifactKinds, (state: Type) => {
if (!this.artifact || !this.artifact.status) {
return true;
}
return state.id !== this.artifact.status;
})
},
},
methods: {
setArtifactType() {
@@ -229,6 +254,42 @@ export default Vue.extend({
}
});
},
kindIcon: function (kind: string): string {
let icon = "mdi-help";
this.lodash.forEach(this.$store.state.settings.artifactKinds, (state: Type) => {
if (kind === state.id) {
icon = state.icon;
}
})
return icon;
},
kindColor: function (kind: string) {
let color = TypeColorEnum.Info as TypeColorEnum;
this.lodash.forEach(this.$store.state.settings.artifactKinds, (state: Type) => {
if (kind === state.id && state.color) {
color = state.color
}
})
return color;
},
setKind(kind: string) {
if (!this.artifact || !this.artifact.name || this.ticketID === undefined) {
return;
}
let artifact = this.artifact
artifact.kind = kind
API.setArtifact(this.ticketID, this.artifact.name, artifact).then((response) => {
this.$store.dispatch("alertSuccess", { name: "Artifact kind changed", type: "success" })
if (response.data.artifacts) {
this.lodash.forEach(response.data.artifacts, (artifact) => {
if (artifact.name == this.name) {
this.artifact = artifact;
}
})
}
});
},
load() {
this.loadArtifact(this.ticketID, this.name);
}

177
ui/src/views/Settings.vue Normal file
View File

@@ -0,0 +1,177 @@
<template>
<v-main class="ma-4">
<div v-if="settings !== undefined">
<v-text-field label="Time Format" v-model="settings.timeformat"></v-text-field>
<v-subheader class="mx-0 px-0">Artifact States</v-subheader>
<v-card v-for="state in settings.artifactStates" :key="state.id" class="d-flex mb-2">
<v-row class="px-4 pt-2" dense>
<v-col>
<v-text-field label="ID" v-model="state.id"></v-text-field>
</v-col>
<v-col>
<v-text-field label="Name" v-model="state.name"></v-text-field>
</v-col>
<v-col>
<v-text-field label="Icon" v-model="state.icon"></v-text-field>
</v-col>
<v-col>
<v-select label="Color" v-model="state.color" :items="['info', 'error', 'success', 'warning']" clearable></v-select>
</v-col>
</v-row>
<v-btn icon class="mt-6 mr-4" @click="removeState(state.id)"><v-icon>mdi-close</v-icon></v-btn>
</v-card>
<v-card class="d-flex mb-2">
<v-row class="px-4 pt-2" dense>
<v-col>
<v-text-field label="ID" v-model="newState.id"></v-text-field>
</v-col>
<v-col>
<v-text-field label="Name" v-model="newState.name"></v-text-field>
</v-col>
<v-col>
<v-text-field label="Icon" v-model="newState.icon"></v-text-field>
</v-col>
<v-col>
<v-select label="Color" v-model="newState.color" :items="['info', 'error', 'success', 'warning']" clearable></v-select>
</v-col>
</v-row>
<v-btn icon class="mt-6 mr-4" @click="addState"><v-icon>mdi-plus</v-icon></v-btn>
</v-card>
<v-subheader class="mx-0 px-0">Artifact Types</v-subheader>
<v-card v-for="state in settings.artifactKinds" :key="state.id" class="d-flex mb-2">
<v-row class="px-4 pt-2" dense>
<v-col>
<v-text-field label="ID" v-model="state.id"></v-text-field>
</v-col>
<v-col>
<v-text-field label="Name" v-model="state.name"></v-text-field>
</v-col>
<v-col>
<v-text-field label="Icon" v-model="state.icon"></v-text-field>
</v-col>
<v-col>
<v-select label="Color" v-model="state.color" :items="['info', 'error', 'success', 'warning']" clearable></v-select>
</v-col>
</v-row>
<v-btn icon class="mt-6 mr-4" @click="removeKind(state.id)"><v-icon>mdi-close</v-icon></v-btn>
</v-card>
<v-card class="d-flex mb-2">
<v-row class="px-4 pt-2" dense>
<v-col>
<v-text-field label="ID" v-model="newKind.id"></v-text-field>
</v-col>
<v-col>
<v-text-field label="Name" v-model="newKind.name"></v-text-field>
</v-col>
<v-col>
<v-text-field label="Icon" v-model="newKind.icon"></v-text-field>
</v-col>
<v-col>
<v-select label="Color" v-model="newKind.color" :items="['info', 'error', 'success', 'warning']" clearable></v-select>
</v-col>
</v-row>
<v-btn icon class="mt-6 mr-4" @click="addKind"><v-icon>mdi-plus</v-icon></v-btn>
</v-card>
<v-btn color="success" @click="save" outlined class="mt-2">
<v-icon>mdi-content-save</v-icon>
Save
</v-btn>
</div>
</v-main>
</template>
<script lang="ts">
import {DateTime} from "luxon";
import Vue from "vue";
import {Settings, SettingsResponse, Type} from "@/client";
import {API} from "@/services/api";
import {AxiosResponse} from "axios";
interface State {
valid: boolean;
settings?: Settings;
newState: Type;
newKind: Type;
}
export default Vue.extend({
name: "Settings",
data: (): State => ({
valid: true,
settings: undefined,
newState: {} as Type,
newKind: {} as Type,
}),
methods: {
save: function () {
if (this.settings === undefined) {
return
}
API.saveSettings(this.settings).then((response) => {
this.settings = response.data;
this.$store.dispatch("getSettings");
})
},
addState: function () {
if (this.settings === undefined) {
return
}
this.settings.artifactStates.push(this.newState);
this.newState = {} as Type;
},
removeState: function (id: string) {
if (this.settings === undefined) {
return
}
this.settings.artifactStates = this.lodash.filter(this.settings.artifactStates, function (t) { return t.id !== id });
},
addKind: function () {
if (this.settings === undefined) {
return
}
this.settings.artifactKinds.push(this.newKind);
this.newKind = {} as Type;
},
removeKind: function (id: string) {
if (this.settings === undefined) {
return
}
this.settings.artifactKinds = this.lodash.filter(this.settings.artifactKinds, function (t) { return t.id !== id });
},
timeformat: function (s: string) {
let format = this.$store.state.settings.timeformat;
if (!format) {
return DateTime.fromISO(s).toLocaleString(DateTime.DATETIME_SHORT);
}
return DateTime.fromISO(s).toFormat(format);
},
dateformat: function (s: string) {
let format = this.$store.state.settings.timeformat;
if (!format) {
return DateTime.fromISO(s).toLocaleString(DateTime.DATETIME_SHORT);
}
return DateTime.fromISO(s).toFormat(format);
},
datetimeformat: function (s: string) {
let format = this.$store.state.settings.timeformat;
if (!format) {
return DateTime.fromISO(s).toLocaleString(DateTime.DATETIME_SHORT);
}
return DateTime.fromISO(s).toFormat(format);
},
},
mounted() {
API.getSettings().then((response: AxiosResponse<SettingsResponse>) => {
this.settings = response.data;
})
}
});
</script>

View File

@@ -181,7 +181,7 @@
<v-jsf
v-model="ticket.details"
:schema="schema"
:options="{ initialValidation: 'all', formats: { time: timeformat, date: dateformat, 'date-time': datetimeformat } }"
:options="{ initialValidation: 'all', formats: { time: timeformat, date: dateformat, 'date-time': datetimeformat }, editMode: 'inline' }"
/>
</v-form>
<v-btn small class="float-right mb-2" color="card" @click="saveTicket" outlined>