From 5984cef96b36a9d0ca330f48684ff12c0727223c Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Mon, 15 Jan 2024 14:07:32 -0600 Subject: [PATCH] config: add Service.RequiredLabels (#3531) * add config entry and start updating form * handle req. labels in edit * don't break editing flows * validate labels on config update * show label value errors * update label --------- Co-authored-by: Nathaniel Cook --- config/config.go | 11 ++++ graphql2/mapconfig.go | 4 ++ web/src/app/services/ServiceCreateDialog.tsx | 6 +- web/src/app/services/ServiceEditDialog.tsx | 46 +++++++++++--- web/src/app/services/ServiceForm.tsx | 64 +++++++++++++++++++- web/src/schema.d.ts | 1 + 6 files changed, 122 insertions(+), 10 deletions(-) diff --git a/config/config.go b/config/config.go index 0dca547a27..3b917066ac 100644 --- a/config/config.go +++ b/config/config.go @@ -34,6 +34,10 @@ type Config struct { DisableCalendarSubscriptions bool `public:"true" info:"If set, disables all active calendar subscriptions as well as the ability to create new calendar subscriptions."` } + Services struct { + RequiredLabels []string `public:"true" info:"List of label names to require new services to define."` + } + Maintenance struct { AlertCleanupDays int `public:"true" info:"Closed alerts will be deleted after this many days (0 means disable cleanup)."` AlertAutoCloseDays int `public:"true" info:"Unacknowledged alerts will automatically be closed after this many days of inactivity. (0 means disable auto-close)."` @@ -447,6 +451,12 @@ func (cfg Config) Validate() error { } return validate.OAuthScope(fname, val, "openid") } + validateLabels := func(fname string, vals []string) (err error) { + for i, v := range vals { + err = validate.Many(err, validate.LabelKey(fmt.Sprintf("%s[%d]", fname, i), v)) + } + return err + } err = validate.Many( err, @@ -456,6 +466,7 @@ func (cfg Config) Validate() error { validateKey("Slack.ClientSecret", cfg.Slack.ClientSecret), validateKey("Twilio.AccountSID", cfg.Twilio.AccountSID), validateKey("Twilio.AuthToken", cfg.Twilio.AuthToken), + validateLabels("Services.RequiredLabels", cfg.Services.RequiredLabels), validateKey("Twilio.AlternateAuthToken", cfg.Twilio.AlternateAuthToken), validate.ASCII("Twilio.VoiceName", cfg.Twilio.VoiceName, 0, 50), validate.ASCII("Twilio.VoiceLanguage", cfg.Twilio.VoiceLanguage, 0, 10), diff --git a/graphql2/mapconfig.go b/graphql2/mapconfig.go index ab870c14cd..746aacf7ea 100644 --- a/graphql2/mapconfig.go +++ b/graphql2/mapconfig.go @@ -34,6 +34,7 @@ func MapConfigValues(cfg config.Config) []ConfigValue { {ID: "General.DisableSMSLinks", Type: ConfigTypeBoolean, Description: "If set, SMS messages will not contain a URL pointing to GoAlert.", Value: fmt.Sprintf("%t", cfg.General.DisableSMSLinks)}, {ID: "General.DisableLabelCreation", Type: ConfigTypeBoolean, Description: "Disables the ability to create new labels for services.", Value: fmt.Sprintf("%t", cfg.General.DisableLabelCreation)}, {ID: "General.DisableCalendarSubscriptions", Type: ConfigTypeBoolean, Description: "If set, disables all active calendar subscriptions as well as the ability to create new calendar subscriptions.", Value: fmt.Sprintf("%t", cfg.General.DisableCalendarSubscriptions)}, + {ID: "Services.RequiredLabels", Type: ConfigTypeStringList, Description: "List of label names to require new services to define.", Value: strings.Join(cfg.Services.RequiredLabels, "\n")}, {ID: "Maintenance.AlertCleanupDays", Type: ConfigTypeInteger, Description: "Closed alerts will be deleted after this many days (0 means disable cleanup).", Value: fmt.Sprintf("%d", cfg.Maintenance.AlertCleanupDays)}, {ID: "Maintenance.AlertAutoCloseDays", Type: ConfigTypeInteger, Description: "Unacknowledged alerts will automatically be closed after this many days of inactivity. (0 means disable auto-close).", Value: fmt.Sprintf("%d", cfg.Maintenance.AlertAutoCloseDays)}, {ID: "Maintenance.APIKeyExpireDays", Type: ConfigTypeInteger, Description: "Unused calendar API keys will be disabled after this many days (0 means disable cleanup).", Value: fmt.Sprintf("%d", cfg.Maintenance.APIKeyExpireDays)}, @@ -103,6 +104,7 @@ func MapPublicConfigValues(cfg config.Config) []ConfigValue { {ID: "General.DisableSMSLinks", Type: ConfigTypeBoolean, Description: "If set, SMS messages will not contain a URL pointing to GoAlert.", Value: fmt.Sprintf("%t", cfg.General.DisableSMSLinks)}, {ID: "General.DisableLabelCreation", Type: ConfigTypeBoolean, Description: "Disables the ability to create new labels for services.", Value: fmt.Sprintf("%t", cfg.General.DisableLabelCreation)}, {ID: "General.DisableCalendarSubscriptions", Type: ConfigTypeBoolean, Description: "If set, disables all active calendar subscriptions as well as the ability to create new calendar subscriptions.", Value: fmt.Sprintf("%t", cfg.General.DisableCalendarSubscriptions)}, + {ID: "Services.RequiredLabels", Type: ConfigTypeStringList, Description: "List of label names to require new services to define.", Value: strings.Join(cfg.Services.RequiredLabels, "\n")}, {ID: "Maintenance.AlertCleanupDays", Type: ConfigTypeInteger, Description: "Closed alerts will be deleted after this many days (0 means disable cleanup).", Value: fmt.Sprintf("%d", cfg.Maintenance.AlertCleanupDays)}, {ID: "Maintenance.AlertAutoCloseDays", Type: ConfigTypeInteger, Description: "Unacknowledged alerts will automatically be closed after this many days of inactivity. (0 means disable auto-close).", Value: fmt.Sprintf("%d", cfg.Maintenance.AlertAutoCloseDays)}, {ID: "Maintenance.APIKeyExpireDays", Type: ConfigTypeInteger, Description: "Unused calendar API keys will be disabled after this many days (0 means disable cleanup).", Value: fmt.Sprintf("%d", cfg.Maintenance.APIKeyExpireDays)}, @@ -188,6 +190,8 @@ func ApplyConfigValues(cfg config.Config, vals []ConfigValueInput) (config.Confi return cfg, err } cfg.General.DisableCalendarSubscriptions = val + case "Services.RequiredLabels": + cfg.Services.RequiredLabels = parseStringList(v.Value) case "Maintenance.AlertCleanupDays": val, err := parseInt(v.ID, v.Value) if err != nil { diff --git a/web/src/app/services/ServiceCreateDialog.tsx b/web/src/app/services/ServiceCreateDialog.tsx index 96585cd40d..18093af6d0 100644 --- a/web/src/app/services/ServiceCreateDialog.tsx +++ b/web/src/app/services/ServiceCreateDialog.tsx @@ -4,12 +4,14 @@ import { fieldErrors, nonFieldErrors } from '../util/errutil' import FormDialog from '../dialogs/FormDialog' import ServiceForm, { Value } from './ServiceForm' import { Redirect } from 'wouter' +import { Label } from '../../schema' interface InputVar { name: string description: string escalationPolicyID?: string favorite: boolean + labels: Label[] newEscalationPolicy?: { name: string description: string @@ -30,7 +32,7 @@ const createMutation = gql` ` function inputVars( - { name, description, escalationPolicyID }: Value, + { name, description, escalationPolicyID, labels }: Value, attempt = 0, ): InputVar { const vars: InputVar = { @@ -38,6 +40,7 @@ function inputVars( description, escalationPolicyID, favorite: true, + labels, } if (!vars.escalationPolicyID) { vars.newEscalationPolicy = { @@ -68,6 +71,7 @@ export default function ServiceCreateDialog(props: { name: '', description: '', escalationPolicyID: '', + labels: [], }) const [createKeyStatus, commit] = useMutation(createMutation) diff --git a/web/src/app/services/ServiceEditDialog.tsx b/web/src/app/services/ServiceEditDialog.tsx index d8948556b1..25eb0339bc 100644 --- a/web/src/app/services/ServiceEditDialog.tsx +++ b/web/src/app/services/ServiceEditDialog.tsx @@ -6,11 +6,13 @@ import { fieldErrors, nonFieldErrors } from '../util/errutil' import FormDialog from '../dialogs/FormDialog' import ServiceForm from './ServiceForm' import Spinner from '../loading/components/Spinner' +import { Label } from '../../schema' interface Value { name: string description: string escalationPolicyID?: string + labels: Label[] } const query = gql` @@ -19,6 +21,10 @@ const query = gql` id name description + labels { + key + value + } ep: escalationPolicy { id name @@ -31,6 +37,11 @@ const mutation = gql` updateService(input: $input) } ` +const setLabel = gql` + mutation setLabel($input: SetLabelInput!) { + setLabel(input: $input) + } +` export default function ServiceEditDialog(props: { serviceID: string @@ -43,6 +54,7 @@ export default function ServiceEditDialog(props: { }) const [saveStatus, save] = useMutation(mutation) + const [saveLabelStatus, saveLabel] = useMutation(setLabel) if (dataFetching && !data) { return @@ -52,9 +64,12 @@ export default function ServiceEditDialog(props: { name: data?.service?.name, description: data?.service?.description, escalationPolicyID: data?.service?.ep?.id, + labels: data?.service?.labels || [], } - const fieldErrs = fieldErrors(saveStatus.error) + const fieldErrs = fieldErrors(saveStatus.error).concat( + fieldErrors(saveLabelStatus.error), + ) return ( { - save( + onSubmit={async () => { + const saveRes = await save( { input: { - ...value, id: props.serviceID, + name: value?.name || '', + description: value?.description || '', + escalationPolicyID: value?.escalationPolicyID || '', }, }, { additionalTypenames: ['Service'], }, - ).then((res) => { + ) + if (saveRes.error) return + + for (const label of value?.labels || []) { + const res = await saveLabel({ + input: { + target: { + type: 'service', + id: props.serviceID, + }, + key: label.key, + value: label.value, + }, + }) if (res.error) return - props.onClose() - }) + } + + props.onClose() }} form={ { + if (e.field !== 'value') { + // label value + return e + } + return { + ...e, + field: 'labels', + } + }) + + const [reqLabels] = useConfigValue('Services.RequiredLabels') as [string[]] + return ( - + + {reqLabels && + reqLabels.map((labelName: string, idx: number) => ( + + 1 && idx === 0 + ? 'Service Labels' + : '' + } + InputProps={{ + startAdornment: ( + + {labelName}: + + ), + }} + mapOnChangeValue={(newVal: string, value: Value) => { + return [ + ...value.labels.filter((l) => l.key !== labelName), + { + key: labelName, + value: newVal, + }, + ] + }} + mapValue={(labels: Label[]) => + labels.find((l) => l.key === labelName)?.value || '' + } + /> + + ))} ) diff --git a/web/src/schema.d.ts b/web/src/schema.d.ts index 4269ff2e40..3fc5506268 100644 --- a/web/src/schema.d.ts +++ b/web/src/schema.d.ts @@ -1397,6 +1397,7 @@ type ConfigID = | 'General.DisableSMSLinks' | 'General.DisableLabelCreation' | 'General.DisableCalendarSubscriptions' + | 'Services.RequiredLabels' | 'Maintenance.AlertCleanupDays' | 'Maintenance.AlertAutoCloseDays' | 'Maintenance.APIKeyExpireDays'