Skip to content

Commit

Permalink
feat(admin): Add ability to edit existing runtime config (#2297)
Browse files Browse the repository at this point in the history
Right now editing is only possible if the current and new types
are the same. It does not currently support passing the "force"
option which is required if the evaluated type is not the same as before.
  • Loading branch information
lynnagara authored Dec 17, 2021
1 parent 0ae26a5 commit 83b94b6
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 12 deletions.
2 changes: 1 addition & 1 deletion snuba/admin/dist/bundle.js

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions snuba/admin/static/api_client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface Client {
getConfigs: () => Promise<Config[]>;
createNewConfig: (key: ConfigKey, value: ConfigValue) => Promise<Config>;
deleteConfig: (key: ConfigKey) => Promise<void>;
editConfig: (key: ConfigKey, value: ConfigValue) => Promise<Config>;
getAuditlog: () => Promise<ConfigChange[]>;
getClickhouseNodes: () => Promise<[ClickhouseNodeData]>;
getClickhouseCannedQueries: () => Promise<[ClickhouseCannedQuery]>;
Expand Down Expand Up @@ -62,6 +63,21 @@ function Client() {
}
});
},
editConfig: (key: ConfigKey, value: ConfigValue) => {
const url = baseUrl + "configs/" + encodeURIComponent(key);
return fetch(url, {
headers: { "Content-Type": "application/json" },
method: "PUT",
body: JSON.stringify({ value }),
}).then((res) => {
if (res.ok) {
return Promise.resolve(res.json());
} else {
throw new Error("Could not edit config");
}
});
},

getAuditlog: () => {
const url = baseUrl + "config_auditlog";
return fetch(url).then((resp) => resp.json());
Expand Down
29 changes: 28 additions & 1 deletion snuba/admin/static/runtime_config/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,34 @@ function RuntimeConfig(props: { api: Client }) {
return { ...prev, value: newValue };
});
},
() => {}, // TODO: Editing existing row
() => {
if (
window.confirm(
`Are you sure you want to update ${key} to ${currentRowData.value}?`
)
) {
api
.editConfig(key, currentRowData.value)
.then((res) => {
setData((prev) => {
if (prev) {
const row = prev.find(
(config) => config.key === res.key
);
if (!row) {
throw new Error("An error occurred");
}
row.value = res.value;
}
return prev;
});
resetForm();
})
.catch((err) => {
window.alert(err);
});
}
},
() => {
if (window.confirm(`Are you sure you want to delete ${key}?`)) {
api.deleteConfig(key).then(() => {
Expand Down
8 changes: 3 additions & 5 deletions snuba/admin/static/runtime_config/row_data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,13 @@ function getEditableRow(
): RowData {
return [
key,
<code>{value}</code>,
// TODO: Edit mode
// <EditableTableCell value={value} onChange={updateValue} />,
<EditableTableCell value={value} onChange={updateValue} />,
type,
<span>
{/* <a style={linkStyle} onClick={() => save()}>
<a style={linkStyle} onClick={() => save()}>
<strong>save changes</strong>
</a>
<Space /> */}
<Space />
<a style={{ ...linkStyle, color: "red" }} onClick={() => deleteRow()}>
delete
</a>
Expand Down
53 changes: 48 additions & 5 deletions snuba/admin/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@

notification_client = RuntimeConfigAutoClient()

USER_HEADER_KEY = "X-Goog-Authenticated-User-Email"


@application.route("/")
def root() -> Response:
Expand Down Expand Up @@ -140,7 +142,7 @@ def configs() -> Response:
{"Content-Type": "application/json"},
)

user = request.headers.get("X-Goog-Authenticated-User-Email")
user = request.headers.get(USER_HEADER_KEY)

state.set_config(
key, value, user=user,
Expand Down Expand Up @@ -176,10 +178,10 @@ def configs() -> Response:
)


@application.route("/configs/<config_key>", methods=["DELETE"])
@application.route("/configs/<config_key>", methods=["PUT", "DELETE"])
def config(config_key: str) -> Response:
if request.method == "DELETE":
user = request.headers.get("X-Goog-Authenticated-User-Email")
user = request.headers.get(USER_HEADER_KEY)
state.delete_config(config_key, user=user)

notification_client.notify(
Expand All @@ -190,8 +192,49 @@ def config(config_key: str) -> Response:

return Response("", 200)

# TODO: Editing existing config
raise NotImplementedError
else:
# PUT currently only supports editing existing config when old and
# new types match. Does not currently support passing force to
# set_config to override the type check.

user = request.headers.get(USER_HEADER_KEY)
data = json.loads(request.data)
try:
new_value = data["value"]

assert isinstance(config_key, str), "Invalid key"
assert isinstance(new_value, str), "Invalid value"
assert config_key != "", "Key cannot be empty string"

state.set_config(
config_key, new_value, user=user,
)

except (KeyError, AssertionError) as exc:
return Response(
json.dumps({"error": f"Invalid config: {str(exc)}"}),
400,
{"Content-Type": "application/json"},
)
except (state.MismatchedTypeException):
return Response(
json.dumps({"error": "Mismatched type"}),
400,
{"Content-Type": "application/json"},
)

# Value was updated successfully, refetch and return it
evaluated_value = state.get_uncached_config(config_key)
assert evaluated_value is not None
evaluated_type = get_config_type_from_value(evaluated_value)

config = {
"key": config_key,
"value": str(evaluated_value),
"type": evaluated_type,
}

return Response(json.dumps(config), 200, {"Content-Type": "application/json"})


@application.route("/config_auditlog")
Expand Down

0 comments on commit 83b94b6

Please sign in to comment.