Skip to content

Commit

Permalink
Show and modify routing rules from the UI
Browse files Browse the repository at this point in the history
  • Loading branch information
prakhar10 committed Aug 9, 2024
1 parent e6b45cf commit e09c3b9
Show file tree
Hide file tree
Showing 8 changed files with 349 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.trino.gateway.ha.domain;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

import java.util.List;

/**
* RoutingRules
*
* @param name name of the routing rule
* @param description description of the routing rule
* @param priority priority of the routing rule
* @param actions actions of the routing rule
* @param condition condition of the routing rule
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record RoutingRules(
@JsonProperty("name") String name,
@JsonProperty("description") String description,
@JsonProperty("priority") Integer priority,
@JsonProperty("actions") List<String> actions,
@JsonProperty("condition") String condition)
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@
*/
package io.trino.gateway.ha.resource;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLParser;
import com.google.common.base.Strings;
import com.google.inject.Inject;
import io.trino.gateway.ha.clustermonitor.ClusterStats;
import io.trino.gateway.ha.config.HaGatewayConfiguration;
import io.trino.gateway.ha.config.ProxyBackendConfiguration;
import io.trino.gateway.ha.domain.Result;
import io.trino.gateway.ha.domain.RoutingRules;
import io.trino.gateway.ha.domain.TableData;
import io.trino.gateway.ha.domain.request.GlobalPropertyRequest;
import io.trino.gateway.ha.domain.request.QueryDistributionRequest;
Expand All @@ -36,6 +41,7 @@
import io.trino.gateway.ha.router.ResourceGroupsManager;
import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
Expand All @@ -44,11 +50,15 @@
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
Expand All @@ -67,18 +77,21 @@ public class GatewayWebAppResource
private final QueryHistoryManager queryHistoryManager;
private final BackendStateManager backendStateManager;
private final ResourceGroupsManager resourceGroupsManager;
private final HaGatewayConfiguration configuration;

@Inject
public GatewayWebAppResource(
GatewayBackendManager gatewayBackendManager,
QueryHistoryManager queryHistoryManager,
BackendStateManager backendStateManager,
ResourceGroupsManager resourceGroupsManager)
ResourceGroupsManager resourceGroupsManager,
HaGatewayConfiguration configuration)
{
this.gatewayBackendManager = requireNonNull(gatewayBackendManager, "gatewayBackendManager is null");
this.queryHistoryManager = requireNonNull(queryHistoryManager, "queryHistoryManager is null");
this.backendStateManager = requireNonNull(backendStateManager, "backendStateManager is null");
this.resourceGroupsManager = requireNonNull(resourceGroupsManager, "resourceGroupsManager is null");
this.configuration = requireNonNull(configuration, "configuration is null");
}

@POST
Expand Down Expand Up @@ -423,4 +436,67 @@ public Response readExactMatchSourceSelector()
List<ResourceGroupsManager.ExactSelectorsDetail> selectorsDetailList = resourceGroupsManager.readExactMatchSourceSelector();
return Response.ok(Result.ok(selectorsDetailList)).build();
}

@GET
@RolesAllowed("USER")
@Produces(MediaType.APPLICATION_JSON)
@Path("/getRoutingRules")
public Response readRoutingRules()
{
String content = null;
try {
String rulesConfigPath = configuration.getRoutingRules().getRulesConfigPath();
content = new String(Files.readAllBytes(Paths.get(rulesConfigPath)));
ObjectMapper yamlReader = new ObjectMapper(new YAMLFactory());
YAMLParser parser = new YAMLFactory().createParser(content);
List<RoutingRules> routingRulesList = new ArrayList<>();
while (parser.nextToken() != null) {
RoutingRules routingRules = yamlReader.readValue(parser, RoutingRules.class);
routingRulesList.add(routingRules);
}
return Response.ok(Result.ok(routingRulesList)).build();
}
catch (IOException e) {
throw new RuntimeException(e);
}
}

@POST
@RolesAllowed("ADMIN")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Path("/updateRoutingRules")
public Response updateRoutingRules(RoutingRules routingRules)
{
String rulesConfigPath = configuration.getRoutingRules().getRulesConfigPath();
ObjectMapper yamlReader = new ObjectMapper(new YAMLFactory());
List<RoutingRules> routingRulesList = new ArrayList<>();

try {
String content = new String(Files.readAllBytes(Paths.get(rulesConfigPath)));
YAMLParser parser = new YAMLFactory().createParser(content);
while (parser.nextToken() != null) {
RoutingRules routingRule = yamlReader.readValue(parser, RoutingRules.class);
routingRulesList.add(routingRule);
}

for (int i = 0; i < routingRulesList.size(); i++) {
if (routingRulesList.get(i).name().equals(routingRules.name())) {
routingRulesList.set(i, routingRules);
break;
}
}

ObjectMapper yamlWriter = new ObjectMapper(new YAMLFactory());
StringBuilder yamlContent = new StringBuilder();
for (RoutingRules rule : routingRulesList) {
yamlContent.append(yamlWriter.writeValueAsString(rule));
}
Files.write(Paths.get(rulesConfigPath), yamlContent.toString().getBytes());
}
catch (IOException e) {
throw new RuntimeException(e);
}
return Response.ok(Result.ok(routingRulesList)).build();
}
}
11 changes: 11 additions & 0 deletions webapp/src/api/webapp/routing-rules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {api} from "../base";
import {RoutingRulesData} from "../../types/routing-rules";

export async function routingRulesApi(): Promise<any> {
const response = await api.get('/webapp/getRoutingRules');
return response;
}

export async function updateRoutingRulesApi(body: Record<string, any>): Promise<RoutingRulesData> {
return api.post('/webapp/updateRoutingRules', body)
}
23 changes: 14 additions & 9 deletions webapp/src/components/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Nav, Avatar, Layout, Dropdown, Button, Toast, Modal, Tag } from '@douyinfe/semi-ui';
import { IconGithubLogo, IconDoubleChevronRight, IconDoubleChevronLeft, IconMoon, IconSun, IconMark, IconIdCard } from '@douyinfe/semi-icons';
import { IconGithubLogo, IconDoubleChevronRight, IconDoubleChevronLeft, IconMoon, IconSun, IconMark, IconIdCard, IconUserSetting, IconUser } from '@douyinfe/semi-icons';
import styles from './layout.module.scss';
import { useEffect, useState } from 'react';
import { Link, useLocation } from "react-router-dom";
Expand Down Expand Up @@ -87,14 +87,19 @@ export const RootLayout = (props: {
</Dropdown.Menu>
}
>
<Avatar
size="small"
src={access.avatar || config.avatar}
color="blue"
className={styles.avatar}
>
{access.nickName}
</Avatar>
{access.roles.includes('ADMIN') ? (
<Button icon={<IconUserSetting
size="extra-large"
color="orange"
className={styles.semiIconsBell} />}>
</Button>
) : (
<Button icon={<IconUser
size="extra-large"
color="blue"
className={styles.semiIconsBell} />}>
</Button>
)}
</Dropdown>
</div>
}
Expand Down
177 changes: 177 additions & 0 deletions webapp/src/components/routing-rules.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import {useEffect, useState} from "react";
import {routingRulesApi, updateRoutingRulesApi} from "../api/webapp/routing-rules.ts";
import {RoutingRulesData} from "../types/routing-rules";
import {Button, Card, Form, Toast} from "@douyinfe/semi-ui";
import {FormApi} from "@douyinfe/semi-ui/lib/es/form";
import {Role, useAccessStore} from "../store";

export function RoutingRules() {
const [rules, setRules] = useState<RoutingRulesData[]>([]);
const [editingStates, setEditingStates] = useState<boolean[]>([]);
const [formApis, setFormApis] = useState<(FormApi<any> | null)[]>([]);
const access = useAccessStore();

useEffect(() => {
fetchRoutingRules();
}, []);

const fetchRoutingRules = () => {
routingRulesApi()
.then(data => {
setRules(data);
setEditingStates(new Array(data.length).fill(false));
setFormApis(new Array(data.length).fill(null));
}).catch((error) => {
console.error("Error fetching routing rules: ", error);
Toast.error("Failed to fetch routing rules");
});
};

const handleEdit = (index: number) => {
setEditingStates(prev => {
const newStates = [...prev];
newStates[index] = true;
return newStates;
});
};

const handleSave = async (index: number) => {
const formApi = formApis[index];
if (formApi) {
try {
const values = formApi.getValues();
const actionsArray = Array.isArray(values.actions)
? values.actions.map((action: string) => action.trim())
: [values.actions.trim()];

const updatedRule: RoutingRulesData = {
...rules[index],
...values,
actions: actionsArray
};

await updateRoutingRulesApi(updatedRule);

setEditingStates(prev => {
const newStates = [...prev];
newStates[index] = false;
return newStates;
});

setRules(prev => {
const newRules = [...prev];
newRules[index] = updatedRule;
return newRules;
});

Toast.success("Routing rule updated successfully");
} catch (error) {
console.error("Error updating routing rule: ", error);
Toast.error("Failed to update routing rule");
}
}
};

const setFormApiForIndex = (index: number) => (api: FormApi<any>) => {
setFormApis(prev => {
const newApis = [...prev];
newApis[index] = api;
return newApis;
});
};

return (
<div>
{rules.map((rule, index) => (
<div key={index} style={{marginBottom: '20px'}}>
<Card
shadows='always'
title={`Routing rule #${index + 1}`}
style={{maxWidth: 800, padding: 20}}
bordered={false}
headerExtraContent={
(access.hasRole(Role.ADMIN) && (
<Button onClick={() => handleEdit(index)}>Edit</Button>
))
}
footerStyle={{
display: 'flex',
justifyContent: 'flex-end',
...(editingStates[index] ? {} : { display: 'none' })
}}
footer={
(access.hasRole(Role.ADMIN) && (
<Button onClick={() => handleSave(index)}>Save</Button>
))
}
>
<Form
labelPosition="left"
labelAlign="left"
labelWidth={150}
style={{ paddingRight: '20px' }}
getFormApi={setFormApiForIndex(index)}
>
<Form.Input
field="name"
label="Name"
style={{ width: 600 }}
rules={[
{required: true, message: 'required error'},
{type: 'string', message: 'type error'},
]}
disabled={true}
initValue={rule.name}
/>
<Form.Input
field="description"
label="Description"
style={{ width: 600 }}
rules={[
{required: false, message: 'required error'},
{type: 'string', message: 'type error'},
]}
disabled={!editingStates[index]}
initValue={rule.description}
/>
<Form.Input
field="priority"
label="Priority"
style={{ width: 600 }}
rules={[
{required: false, message: 'required error'},
{type: 'string', message: 'type error'},
]}
disabled={!editingStates[index]}
initValue={rule.priority}
/>
<Form.Input
field="condition"
label="Condition"
style={{ width: 600 }}
rules={[
{required: true, message: 'required error'},
{type: 'string', message: 'type error'},
]}
disabled={!editingStates[index]}
initValue={rule.condition}
/>
<Form.TextArea
field="actions"
label="Actions"
style={{ width: 600, overflowY: 'auto', overflowX: 'auto' }}
autosize rows={1}
rules={[
{required: true, message: 'required error'},
{type: 'string', message: 'type error'},
]}
disabled={!editingStates[index]}
initValue={rule.actions}
/>
</Form>
</Card>
</div>
))}
</div>
);
}
Loading

0 comments on commit e09c3b9

Please sign in to comment.