-
Notifications
You must be signed in to change notification settings - Fork 0
Added hazelcast integration #381
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c06ed80
b2c9c17
8dbbe22
60676c4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -61,6 +61,7 @@ | |||||||||||
| <properties> | ||||||||||||
| <revision>3.0.0-SNAPSHOT</revision> | ||||||||||||
| <java.version>21</java.version> | ||||||||||||
| <spring-framework.version>7.0.6</spring-framework.version> | ||||||||||||
| <jackson.version>2.20.1</jackson.version> | ||||||||||||
| <lombok.version>1.18.42</lombok.version> | ||||||||||||
| <spotless.version>3.0.0</spotless.version> | ||||||||||||
|
|
@@ -96,6 +97,11 @@ | |||||||||||
| </dependencyManagement> | ||||||||||||
|
|
||||||||||||
| <dependencies> | ||||||||||||
| <dependency> | ||||||||||||
| <groupId>org.springframework</groupId> | ||||||||||||
| <artifactId>spring-beans</artifactId> | ||||||||||||
| <version>${spring-framework.version}</version> | ||||||||||||
| </dependency> | ||||||||||||
|
Comment on lines
+100
to
+104
|
||||||||||||
| <dependency> | |
| <groupId>org.springframework</groupId> | |
| <artifactId>spring-beans</artifactId> | |
| <version>${spring-framework.version}</version> | |
| </dependency> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ import { | |
| } from '~/services/project-service' | ||
| import { useRecentProjects } from '~/hooks/use-projects' | ||
| import { showErrorToast } from '~/components/toast' | ||
| import { discoverFrankInstances, type FrankInstance } from '~/services/hazelcast-service' | ||
|
|
||
| export default function ProjectLanding() { | ||
| const navigate = useNavigate() | ||
|
|
@@ -40,6 +41,8 @@ export default function ProjectLanding() { | |
| const [isLocalEnvironment, setIsLocalEnvironment] = useState(true) | ||
| const [rootLocationName, setRootLocationName] = useState('Computer') | ||
| const [isOpeningProject, setIsOpeningProject] = useState(false) | ||
| const [frankInstances, setFrankInstances] = useState<FrankInstance[]>([]) | ||
| const [isDiscovering, setIsDiscovering] = useState(false) | ||
| const importInputRef = useRef<HTMLInputElement>(null) | ||
|
|
||
| useEffect(() => { | ||
|
|
@@ -67,6 +70,29 @@ export default function ProjectLanding() { | |
| } | ||
| }, [apiError]) | ||
|
|
||
| useEffect(() => { | ||
| if (!isLocalEnvironment) return | ||
| const controller = new AbortController() | ||
|
|
||
| const discover = () => { | ||
| discoverFrankInstances(controller.signal) | ||
| .then(setFrankInstances) | ||
| .catch(() => { | ||
| // Discovery failure is non-critical; Hazelcast may not be running | ||
| }) | ||
| .finally(() => setIsDiscovering(false)) | ||
| } | ||
|
|
||
| setIsDiscovering(true) | ||
| discover() | ||
| const interval = setInterval(discover, 3000) | ||
|
|
||
|
stijnpotters1 marked this conversation as resolved.
|
||
| return () => { | ||
| controller.abort() | ||
| clearInterval(interval) | ||
| } | ||
| }, [isLocalEnvironment]) | ||
|
Comment on lines
+73
to
+94
|
||
|
|
||
| const handleOpenProject = useCallback( | ||
| async (rootPath: string) => { | ||
| setIsOpeningProject(true) | ||
|
|
@@ -155,6 +181,27 @@ export default function ProjectLanding() { | |
| } | ||
| } | ||
|
|
||
| const handleConnectToInstance = useCallback( | ||
| async (instance: FrankInstance) => { | ||
| const path = instance.projectPath | ||
| if (!path) { | ||
| showErrorToast(`No configuration path available for "${instance.name}"`) | ||
| return | ||
| } | ||
| setIsOpeningProject(true) | ||
| try { | ||
| const project = await openProject(path) | ||
| setProject(project) | ||
| navigate(`/studio/${encodeURIComponent(project.name)}`) | ||
| } catch (error) { | ||
| showErrorToast(error instanceof Error ? error.message : `Failed to open remote instance "${instance.name}"`) | ||
| } finally { | ||
| setIsOpeningProject(false) | ||
| } | ||
| }, | ||
| [navigate, setProject], | ||
| ) | ||
|
|
||
| const projects = recentProjects ?? [] | ||
| const filteredProjects = projects.filter((project) => project.name.toLowerCase().includes(searchTerm.toLowerCase())) | ||
|
|
||
|
|
@@ -183,6 +230,9 @@ export default function ProjectLanding() { | |
| onProjectClick={handleOpenProject} | ||
| onRemoveProject={onRemoveProject} | ||
| onExportProject={onExportProject} | ||
| frankInstances={frankInstances} | ||
| isDiscovering={isDiscovering} | ||
| onConnectToInstance={handleConnectToInstance} | ||
| /> | ||
| </div> | ||
|
|
||
|
|
@@ -264,27 +314,58 @@ const ProjectList = ({ | |
| onProjectClick, | ||
| onRemoveProject, | ||
| onExportProject, | ||
| frankInstances, | ||
| isDiscovering, | ||
| onConnectToInstance, | ||
| }: { | ||
| projects: RecentProject[] | ||
| isLocal: boolean | ||
| onProjectClick: (rootPath: string) => void | ||
| onRemoveProject: (rootPath: string) => void | ||
| onExportProject: (projectName: string) => void | ||
| frankInstances: FrankInstance[] | ||
| isDiscovering: boolean | ||
| onConnectToInstance: (instance: FrankInstance) => void | ||
| }) => ( | ||
| <section className="h-full flex-1 overflow-y-auto p-4"> | ||
| {projects.length === 0 ? ( | ||
| {frankInstances.length > 0 && ( | ||
| <div className="mb-4"> | ||
| <p className="mb-2 text-xs font-semibold tracking-wider text-slate-500 uppercase">Remote</p> | ||
| {frankInstances.map((instance) => ( | ||
| <div | ||
| key={instance.id ?? instance.name} | ||
| className="hover:bg-backdrop mb-2 flex w-full cursor-pointer items-center justify-between rounded px-3 py-2" | ||
| onClick={() => onConnectToInstance(instance)} | ||
| > | ||
| <div className="flex flex-col"> | ||
| <div className="font-medium">{instance.name}</div> | ||
| {instance.projectPath && <p className="text-foreground-muted text-xs">{instance.projectPath}</p>} | ||
| </div> | ||
| <span className="rounded bg-green-100 px-2 py-0.5 text-xs text-green-700">Live</span> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| )} | ||
| {isDiscovering && frankInstances.length === 0 && ( | ||
| <p className="mb-2 text-xs text-slate-400 italic">Scanning for remote instances...</p> | ||
| )} | ||
| {projects.length === 0 && frankInstances.length === 0 && !isDiscovering && ( | ||
| <p className="text-muted-foreground mt-10 text-center text-sm italic">No projects found</p> | ||
| ) : ( | ||
| projects.map((project) => ( | ||
| <ProjectRow | ||
| key={project.rootPath} | ||
| project={project} | ||
| isLocal={isLocal} | ||
| onClick={() => onProjectClick(project.rootPath)} | ||
| onRemove={() => onRemoveProject(project.rootPath)} | ||
| onExport={() => onExportProject(project.name)} | ||
| /> | ||
| )) | ||
| )} | ||
| {projects.length > 0 && ( | ||
| <> | ||
| <p className="mb-2 text-xs font-semibold tracking-wider text-slate-400 uppercase">Recent</p> | ||
| {projects.map((project) => ( | ||
| <ProjectRow | ||
| key={project.rootPath} | ||
| project={project} | ||
| isLocal={isLocal} | ||
| onClick={() => onProjectClick(project.rootPath)} | ||
| onRemove={() => onRemoveProject(project.rootPath)} | ||
| onExport={() => onExportProject(project.name)} | ||
| /> | ||
| ))} | ||
| </> | ||
| )} | ||
| </section> | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import { apiFetch } from '~/utils/api' | ||
|
|
||
| export interface FrankInstance { | ||
| name: string | ||
| id: string | ||
| projectPath: string | null | ||
| } | ||
|
|
||
| export async function discoverFrankInstances(signal?: AbortSignal): Promise<FrankInstance[]> { | ||
| return apiFetch<FrankInstance[]>('/hazelcast/instances', { signal }) | ||
| } |
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ? |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,31 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| package org.frankframework.flow; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Copyright 2023 WeAreFrank! | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.boot.builder.SpringApplicationBuilder; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Spring Boot entrypoint when running as a WAR application deployed to an external servlet container. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * For standalone JAR execution, see {@link FlowApplication}. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class FlowWarInitializer extends SpringBootServletInitializer { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return builder.sources(FlowApplication.class); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+18
to
+31
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.boot.builder.SpringApplicationBuilder; | |
| import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; | |
| /** | |
| * Spring Boot entrypoint when running as a WAR application deployed to an external servlet container. | |
| * For standalone JAR execution, see {@link FlowApplication}. | |
| */ | |
| public class FlowWarInitializer extends SpringBootServletInitializer { | |
| @Override | |
| protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { | |
| return builder.sources(FlowApplication.class); | |
| } | |
| } | |
| /** | |
| * Spring Boot entrypoint when running as a WAR application deployed to an external servlet container. | |
| * For standalone JAR execution, see {@link FlowApplication}. | |
| * <p> | |
| * This class is retained only for backward compatibility. Actual initialization | |
| * is handled by {@link FlowApplication}, which extends SpringBootServletInitializer. | |
| */ | |
| public class FlowWarInitializer { | |
| // No-op: initialization is performed by FlowApplication. | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package org.frankframework.flow.common.config; | ||
|
|
||
| import org.apache.commons.lang3.StringUtils; | ||
| import org.frankframework.management.bus.LocalGateway; | ||
| import org.frankframework.management.bus.OutboundGatewayFactory; | ||
| import org.frankframework.management.gateway.HazelcastOutboundGateway; | ||
| import org.frankframework.management.security.JwtKeyGenerator; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.integration.channel.PublishSubscribeChannel; | ||
| import org.springframework.messaging.SubscribableChannel; | ||
|
|
||
| @Configuration | ||
| @ConditionalOnProperty(name = "hazelcast.enabled", havingValue = "true") | ||
| public class HazelcastConfig { | ||
|
|
||
| @Value("${configurations.directory:}") | ||
| private String configurationsDirectory; | ||
|
|
||
| @Bean | ||
| public JwtKeyGenerator jwtKeyGenerator() { | ||
| return new JwtKeyGenerator(); | ||
| } | ||
|
|
||
| @Bean("frank-management-bus") | ||
| @ConditionalOnProperty(name = "configurations.directory") | ||
| public SubscribableChannel frankManagementBus() { | ||
| return new PublishSubscribeChannel(); | ||
| } | ||
|
stijnpotters1 marked this conversation as resolved.
|
||
|
|
||
| @Bean | ||
| public OutboundGatewayFactory outboundGatewayFactory() { | ||
| OutboundGatewayFactory factory = new OutboundGatewayFactory(); | ||
| String gatewayClassName = StringUtils.isNotBlank(configurationsDirectory) | ||
| ? LocalGateway.class.getCanonicalName() | ||
| : HazelcastOutboundGateway.class.getCanonicalName(); | ||
| factory.setGatewayClassname(gatewayClassName); | ||
| return factory; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package org.frankframework.flow.common.config; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.List; | ||
| import org.frankframework.lifecycle.DynamicRegistration.Servlet; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.core.Ordered; | ||
| import org.springframework.core.annotation.Order; | ||
| import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; | ||
| import org.springframework.security.config.annotation.web.builders.HttpSecurity; | ||
| import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | ||
| import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; | ||
| import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer; | ||
| import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; | ||
| import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer; | ||
| import org.springframework.security.core.GrantedAuthority; | ||
| import org.springframework.security.core.authority.SimpleGrantedAuthority; | ||
| import org.springframework.security.web.SecurityFilterChain; | ||
| import org.springframework.security.web.util.matcher.AnyRequestMatcher; | ||
|
|
||
| /** | ||
| * Enable security, although it's anonymous on all endpoints, but at least sets the | ||
| * SecurityContextHolder.getContext().getAuthentication() object. | ||
| */ | ||
| @Configuration | ||
| @EnableWebSecurity | ||
| @EnableMethodSecurity(jsr250Enabled = true, prePostEnabled = false) | ||
| @Order(Ordered.HIGHEST_PRECEDENCE) | ||
| public class SecurityChainConfigurer { | ||
|
|
||
| @Bean | ||
| public SecurityFilterChain configureChain(HttpSecurity http) { | ||
| http.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)); | ||
| http.csrf(CsrfConfigurer::disable); | ||
| http.securityMatcher(AnyRequestMatcher.INSTANCE); | ||
| http.formLogin(FormLoginConfigurer::disable); | ||
| http.logout(LogoutConfigurer::disable); | ||
| http.anonymous(anonymous -> anonymous.authorities(getAuthorities())); | ||
| http.authorizeHttpRequests(requests -> | ||
| requests.requestMatchers(AnyRequestMatcher.INSTANCE).permitAll()); | ||
|
|
||
| return http.build(); | ||
| } | ||
|
|
||
| private List<GrantedAuthority> getAuthorities() { | ||
| return Arrays.stream(Servlet.ALL_IBIS_USER_ROLES) | ||
|
Check failure on line 47 in src/main/java/org/frankframework/flow/common/config/SecurityChainConfigurer.java
|
||
| .map(role -> (GrantedAuthority) new SimpleGrantedAuthority("ROLE_" + role)) | ||
| .toList(); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.