mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 15:14:52 +09:00
17 files changed
+75
-25
No files matched your search
@@ -6,6 +6,11 @@
|
||||
|
||||
- Fixed an unhandled error in Subspace Storage crashing the host.
|
||||
- Fixed an unhandled rejection in disconnect logic crashing the host.
|
||||
- Fixed instance screen in Web UI not updating when an instance was reassigned.
|
||||
- Fixed an error thrown in the rendering of one page in the Web UI causing all pages to become inaccessible.
|
||||
- Fixed Web UI not showing instance saves when the permission to list and subscribe the saves is granted but the user is not a cluster admin.
|
||||
- Similarly fixed Web UI not showing the mod pack create, delete, list and host revoke token actions if the user is not a cluster admin.
|
||||
- Fixed Research Sync applying progress of a previous techology level to the level on the controller when it's the active research during instance startup.
|
||||
|
||||
|
||||
## Version 2.0.0-alpha.16
|
||||
|
||||
@@ -876,9 +876,11 @@ export default class Controller {
|
||||
*/
|
||||
clearSavesOfInstance(instanceId: number) {
|
||||
const updates = [];
|
||||
const now = Date.now();
|
||||
for (const [id, save] of this.saves) {
|
||||
if (save.instanceId === instanceId) {
|
||||
save.isDeleted = true;
|
||||
save.updatedAtMs = now;
|
||||
updates.push(save);
|
||||
this.saves.delete(id);
|
||||
}
|
||||
@@ -939,9 +941,9 @@ export default class Controller {
|
||||
);
|
||||
} else {
|
||||
instance.status = "unassigned";
|
||||
instance.updatedAtMs = Date.now();
|
||||
this.instanceDetailsUpdated([instance]);
|
||||
}
|
||||
instance.updatedAtMs = Date.now();
|
||||
this.instanceDetailsUpdated([instance]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -178,7 +178,10 @@ export default class HostConnection extends BaseConnection {
|
||||
// changed. Hosts also send status updates on assignInstance which
|
||||
// for hacky reason is also used to push config changes and
|
||||
// restablish status after a connection loss.
|
||||
if (instance.status === request.status) {
|
||||
if (
|
||||
instance.status === request.status
|
||||
&& instance.gamePort === request.gamePort
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,12 +19,20 @@ export default class Role {
|
||||
permissions: Type.Array(Type.String()),
|
||||
});
|
||||
|
||||
static remapPermission(permission: string) {
|
||||
// migrate: core.instance.save.list.subscribe was renamed in alpha 17
|
||||
if (permission === "core.instance.save.list.subscribe") {
|
||||
return "core.instance.save.subscribe";
|
||||
}
|
||||
return permission;
|
||||
}
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(
|
||||
json.id,
|
||||
json.name,
|
||||
json.description,
|
||||
new Set(json.permissions),
|
||||
new Set(json.permissions.map(this.remapPermission)),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -361,7 +361,7 @@ export class InstanceSaveDetailsUpdatesEvent {
|
||||
static type = "event" as const;
|
||||
static src = ["instance", "host", "controller"] as const;
|
||||
static dst = ["controller", "control"] as const;
|
||||
static permission = "core.instance.save.list.subscribe" as const;
|
||||
static permission = "core.instance.save.subscribe" as const;
|
||||
|
||||
constructor(
|
||||
public updates: SaveDetails[],
|
||||
|
||||
@@ -239,11 +239,11 @@ definePermission({
|
||||
definePermission({
|
||||
name: "core.instance.save.list",
|
||||
title: "List saves",
|
||||
description: "List the saves currently on the instance.",
|
||||
description: "List the saves currently on instances.",
|
||||
});
|
||||
definePermission({
|
||||
name: "core.instance.save.list.subscribe",
|
||||
title: "Subscribe to save list updatess",
|
||||
name: "core.instance.save.subscribe",
|
||||
title: "Subscribe to save updates",
|
||||
description: "Subscribe to be notifed on updates to the list of saves of instances.",
|
||||
});
|
||||
definePermission({
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Type, Static } from "@sinclair/typebox";
|
||||
import { Link, Event, EventClass, RequestHandler, WebSocketClientConnector, WebSocketBaseConnector } from "./link";
|
||||
import { logger } from "./logging";
|
||||
import { Address, MessageRequest, IControllerUser } from "./data";
|
||||
import isDeepStrictEqual from "./is_deep_strict_equal";
|
||||
|
||||
export type SubscriptionRequestHandler<T> = RequestHandler<SubscriptionRequest, Event<T> | null>;
|
||||
export type EventSubscriberCallback<T> = (updates: T[], synced: boolean) => void
|
||||
@@ -204,6 +205,14 @@ export class EventSubscriber<
|
||||
async _handle(event: EventSubscribable<T, V>) {
|
||||
for (const value of event.updates) {
|
||||
this.lastResponseTimeMs = Math.max(this.lastResponseTimeMs, value.updatedAtMs);
|
||||
// Warn about updates which changes content but don't update updatedAtMs
|
||||
const existing = this.values.get(value.id as K);
|
||||
if (existing && existing.updatedAtMs === value.updatedAtMs && !isDeepStrictEqual(existing, value)) {
|
||||
logger.warn(
|
||||
`${this.Event.name} contains update for value with id ${value.id} that has an identical ` +
|
||||
"updatedAtMs timestamp but differing content"
|
||||
);
|
||||
}
|
||||
if (value.isDeleted) {
|
||||
this.values.delete(value.id as K);
|
||||
} else {
|
||||
|
||||
@@ -104,16 +104,16 @@ export default function App(props: AppProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary Component={ErrorCard}>
|
||||
<ConfigProvider
|
||||
theme={{ algorithm: theme.darkAlgorithm }}
|
||||
>
|
||||
<ConfigProvider
|
||||
theme={{ algorithm: theme.darkAlgorithm }}
|
||||
>
|
||||
<ErrorBoundary Component={ErrorCard}>
|
||||
<ControlContext.Provider value={props.control}>
|
||||
<BrowserRouter basename={webRoot}>
|
||||
{page}
|
||||
</BrowserRouter>
|
||||
</ControlContext.Provider>
|
||||
</ConfigProvider>
|
||||
</ErrorBoundary>
|
||||
</ErrorBoundary>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
@@ -48,7 +48,7 @@ export default function HostViewPage() {
|
||||
|
||||
let hostButtons = <Space>
|
||||
{
|
||||
account.hasPermission("core.host.revoke_access")
|
||||
account.hasPermission("core.host.revoke_token")
|
||||
&& <Popconfirm
|
||||
title={`Revoke tokens of ${host.name}?`}
|
||||
placement="bottomRight"
|
||||
|
||||
@@ -204,7 +204,7 @@ export default function InstanceViewPage() {
|
||||
<InstanceDescription host={host} instance={instance} />
|
||||
|
||||
{
|
||||
account.hasAllPermission("core.instance.save.list", "core.instance.save.list_subscribe")
|
||||
account.hasAllPermission("core.instance.save.list", "core.instance.save.subscribe")
|
||||
&& <SavesList instance={instance} />
|
||||
}
|
||||
{
|
||||
|
||||
@@ -801,7 +801,7 @@ export default function ModPackViewPage() {
|
||||
title={modPack.name}
|
||||
extra={<Space>
|
||||
<ExportButton modPack={modifiedModPack}/>
|
||||
{account.hasPermission("core.mod-pack.delete") && <Popconfirm
|
||||
{account.hasPermission("core.mod_pack.delete") && <Popconfirm
|
||||
title="Delete mod pack and all of its settings?"
|
||||
placement="bottomRight"
|
||||
okText="Delete"
|
||||
|
||||
@@ -186,8 +186,8 @@ export default function ModsPage() {
|
||||
<SectionHeader
|
||||
title="Mod Packs"
|
||||
extra={<Space>
|
||||
{account.hasPermission("core.mod-pack.create") && <ImportModPackButton />}
|
||||
{account.hasPermission("core.mod-pack.create") && <CreateModPackButton />}
|
||||
{account.hasPermission("core.mod_pack.create") && <ImportModPackButton />}
|
||||
{account.hasPermission("core.mod_pack.create") && <CreateModPackButton />}
|
||||
</Space>}
|
||||
/>
|
||||
<Table
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import React, { Fragment, useContext, useEffect, useState } from "react";
|
||||
import { Route, Routes, useNavigate } from "react-router-dom";
|
||||
import { Dropdown, Layout, Menu, MenuProps } from "antd";
|
||||
import UserOutlined from "@ant-design/icons/UserOutlined";
|
||||
@@ -115,12 +115,12 @@ export default function SiteLayout() {
|
||||
{combinedPages.map(({path, sidebarPath, content}) => <Route
|
||||
path={path}
|
||||
key={path}
|
||||
element={<>
|
||||
element={<Fragment key={path}>
|
||||
<SetSidebar path={sidebarPath ? sidebarPath : path} />
|
||||
<ErrorBoundary Component={ErrorPage}>
|
||||
{content}
|
||||
</ErrorBoundary>
|
||||
</>}
|
||||
</Fragment>}
|
||||
/>)}
|
||||
<Route element={<SetSidebar path={null} />} />
|
||||
</Routes>
|
||||
|
||||
@@ -88,7 +88,6 @@ export default function UserViewPage() {
|
||||
onChange={() => setRolesDirty(true)}
|
||||
loading={!roles}
|
||||
mode="multiple"
|
||||
showArrow={true}
|
||||
filterOption={(inputValue:string, option: any): boolean => {
|
||||
let role = roles.get(option.value);
|
||||
return role?.name.toLowerCase().includes(inputValue.toLowerCase()) ?? false;
|
||||
|
||||
@@ -22,6 +22,12 @@ export function useAccount(): UserAccount {
|
||||
};
|
||||
}, [control]);
|
||||
|
||||
function checkPermissionExists(permission: string) {
|
||||
if (!lib.permissions.has(permission)) {
|
||||
throw new Error(`permission ${permission} does not exist`);
|
||||
}
|
||||
}
|
||||
|
||||
function hasPermission(permission: string): boolean {
|
||||
for (let role of roles) {
|
||||
if (role.permissions.includes("core.admin") || role.permissions.includes(permission)) {
|
||||
@@ -38,12 +44,16 @@ export function useAccount(): UserAccount {
|
||||
if (!roles) {
|
||||
return null;
|
||||
}
|
||||
checkPermissionExists(permission);
|
||||
return hasPermission(permission);
|
||||
},
|
||||
hasAnyPermission(...permissions: string[]): boolean | null {
|
||||
if (!roles) {
|
||||
return null;
|
||||
}
|
||||
for (let permission of permissions) {
|
||||
checkPermissionExists(permission);
|
||||
}
|
||||
for (let permission of permissions) {
|
||||
if (hasPermission(permission)) {
|
||||
return true;
|
||||
@@ -55,6 +65,9 @@ export function useAccount(): UserAccount {
|
||||
if (!roles) {
|
||||
return null;
|
||||
}
|
||||
for (let permission of permissions) {
|
||||
checkPermissionExists(permission);
|
||||
}
|
||||
for (let permission of permissions) {
|
||||
if (!hasPermission(permission)) {
|
||||
return false;
|
||||
|
||||
@@ -47,7 +47,7 @@ export const pages: PluginPage[] = [
|
||||
{
|
||||
path: "/mods",
|
||||
sidebarName: "Mods",
|
||||
permission: (account: UserAccount) => account.hasAnyPermission("core.mod.list", "core.mod-pack.list"),
|
||||
permission: (account: UserAccount) => account.hasAnyPermission("core.mod.list", "core.mod_pack.list"),
|
||||
content: <ModsPage />,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -137,10 +137,21 @@ function research_sync.sync_technologies(data)
|
||||
for _, tech_data in pairs(game.json_to_table(data)) do
|
||||
local tech = force.technologies[tech_data[nameIndex]]
|
||||
if tech and tech.level <= tech_data[levelIndex] then
|
||||
tech.level = math.min(tech_data[levelIndex], tech.prototype.max_level)
|
||||
local new_level = math.min(tech_data[levelIndex], tech.prototype.max_level)
|
||||
if new_level ~= tech.level then
|
||||
-- when the level of the current research changes the
|
||||
-- progress is not automatically reset.
|
||||
if force.current_research == tech then
|
||||
force.research_progress = 0
|
||||
end
|
||||
tech.level = new_level
|
||||
end
|
||||
|
||||
local progress
|
||||
if tech_data[researchedIndex] then
|
||||
if force.current_research == tech then
|
||||
force.research_progress = 0
|
||||
end
|
||||
tech.researched = true
|
||||
progress = nil
|
||||
elseif tech_data[progressIndex] then
|
||||
|
||||
Reference in new issue
Block a user