mirror of
https://github.com/SecurityBrewery/catalyst.git
synced 2025-12-21 22:43:08 +01:00
Add global settings (#40)
This commit is contained in:
@@ -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: "",
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
})
|
||||
|
||||
@@ -22,6 +22,23 @@
|
||||
</v-list>
|
||||
</v-menu>
|
||||
·
|
||||
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>
|
||||
·
|
||||
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
177
ui/src/views/Settings.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user