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'