Skip to content
Snippets Groups Projects
Commit 348bc248 authored by Mohammad Torkashvand's avatar Mohammad Torkashvand
Browse files

Added GUI V2

parent f2e7fcbb
Branches
Tags
1 merge request!18Added GUI V2
Pipeline #86659 passed
BACKEND_URL=http://localhost:8080
# You can use the builtin webpack proxy during development which will connect you to BACKEND_URL
REACT_APP_BACKEND_URL=http://127.0.0.1:8080
ENVIRONMENT_NAME=Development
PROCESS_DETAIL_REFETCH_INTERVAL=3000
REACT_APP_CHECK_STATUS_INTERVAL=0
REACT_APP_TRACING_ENABLED=false
REACT_APP_SENTRY_DSN=
REACT_APP_TRACE_SAMPLE_RATE=1
REACT_APP_RELEASE=local
REACT_APP_ENVIRONMENT=local
REACT_APP_TRACING_ORIGINS=*
ORCHESTRATOR_API_HOST=http://localhost:8080
ORCHESTRATOR_API_PATH=/api
ORCHESTRATOR_GRAPHQL_HOST=http://localhost:8080
ORCHESTRATOR_GRAPHQL_PATH=/api/graphql
ORCHESTRATOR_WEBSOCKET_URL=ws://localhost:8080
# Fill in when Enabling OAUTH2
REACT_APP_OAUTH2_ENABLED=False
REACT_APP_OAUTH2_CLIENT_ID=
REACT_APP_OAUTH2_OPENID_CONNECT_URL=
REACT_APP_OAUTH2_SCOPE=
REACT_APP_OPA_BUNDLE_URL=http://localhost:8080/opa/bundles/policy.wasm
AUTH_ACTIVE=true
NEXTAUTH_URL=http://localhost:3000/api/auth
# Needed because some libs misbehave
GENERATE_SOURCEMAP=false
FAST_REFRESH=false
# OIDC Authentication Settings
NEXTAUTH_ID="oidc"
NEXTAUTH_CLIENT_ID="APP-A43E6FB7-1EEF-49CB-95B9-9AFF6FA7EF66"
NEXTAUTH_CLIENT_SECRET="YOUR_OIDC_CLIENT_SECRET"
NEXTAUTH_SECRET="SOhxHLn53mV7ML7y8L6rL5oOQxOVb0V4p2Ez0ZSIuOs=" # openssl rand -base64 32
NEXTAUTH_ISSUER="https://proxy.aai.geant.org"
NEXTAUTH_WELL_KNOWN_OVERRIDE="https://proxy.aai.geant.org/.well-known/openid-configuration"
# docker-compose variables
# KEYCLOAK_ADMIN=admin
# KEYCLOAK_ADMIN_PASSWORD=admin
# KEYCLOAK_PORT=8085
USE_WEBSOCKET=true
USE_THEME_TOGGLE=true
\ No newline at end of file
### THIS FILE IS A REPLICA FROM ORCHESTRATOR-CORE-GUI WITH SOME ADJUSTMENTS
FROM node:18-alpine AS builder
###############################
### BASE LAYER FOR IMAGES BELOW
FROM node:14.21.1-slim AS base
ENV CI=true
ENV CORE_GUI_TAG=10.7.6
# ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED 1
WORKDIR /app
RUN apt update && apt install git -y
RUN git clone --branch 10.7.6 https://github.com/workfloworchestrator/orchestrator-core-gui.git --depth 1
###########################################
### BUILDER LAYER TO PREPARE FOR PRODUCTION
FROM base AS builder
RUN apk update && apk add --no-cache git
WORKDIR /app/orchestrator-core-gui
RUN yarn --network-concurrency 1 --frozen-lockfile
RUN rm -rf src/custom
RUN git clone https://github.com/workfloworchestrator/orchestrator-ui-library.git --branch @orchestrator-ui/orchestrator-ui-components@1.14.2 --single-branch --depth 1
RUN cd /app/orchestrator-ui-library && \
sed -i 's/git@github.com:/https:\/\/github.com\//' .gitmodules && \
git submodule init && \
git submodule update && \
npm install && \
npm update @orchestrator-ui/orchestrator-ui-components && \
npm update @orchestrator-ui/eslint-config-custom && \
npm update @orchestrator-ui/jest-config && \
npm update @orchestrator-ui/tsconfig
# Apply GÉANT customisations
COPY custom src/custom
COPY logo.svg src/images/logo.svg
COPY favicon.ico public/favicon.ico
COPY colors.ts src/stylesheets/emotion/colors.ts
COPY src/utils/policy.ts src/utils/policy.ts
COPY custom/nextauth.ts /app/orchestrator-ui-library/apps/wfo-ui/pages/api/auth/[...nextauth].ts
RUN cd /app/orchestrator-ui-library/ && npm run build
FROM node:18-alpine AS runner
RUN yarn build
# ENV NODE_ENV=production
########################
### IMAGE FOR PRODUCTION
FROM nginx:alpine
COPY --from=builder /app/orchestrator-ui-library/apps/wfo-ui/.next/standalone /app
COPY --from=builder /app/orchestrator-ui-library/apps/wfo-ui/.next/static /app/.next/static
COPY --from=builder /app/orchestrator-ui-library/node_modules /app/node_modules
WORKDIR /app
RUN apk update && apk add wget curl
COPY --from=builder /app/orchestrator-core-gui/default.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/orchestrator-core-gui/build /usr/share/nginx/html
COPY --from=builder /app/orchestrator-core-gui/src/env.js.template .
COPY --from=builder /app/orchestrator-core-gui/src/custom/custom_env.js.template .
EXPOSE 8080
CMD [ "/bin/ash", "-c", "envsubst < env.js.template > /usr/share/nginx/html/env.js && envsubst < custom_env.js.template >> /usr/share/nginx/html/env.js && exec nginx -g 'daemon off;'"]
USER node
EXPOSE 3000
CMD ["node", "server.js", "-H", "0.0.0.0"]
/* Default font colors and table colors */
export const DARK_FONT_COLOR = "#ffffff";
export const LIGHT_FONT_COLOR = "#111111";
export const DARK_ROW_BORDER_COLOR = "#555555";
export const LIGHT_ROW_BORDER_COLOR = "#cccccc";
export const DARK_BACKGROUND_COLOR = "#1D1E24";
export const LIGHT_BACKGROUND_COLOR = "#f6f6f6";
export const DARK_SELECTED_FONT_COLOR = "#81a0af";
export const LIGHT_SELECTED_FONT_COLOR = "#4d798f";
/* Copy of EUI colors */
export const PRIMARY_COLOR = "#ed1556";
export const ACCENT = "#003f5f";
export const SUCCESS = "#3aca38";
export const WARNING = "#e39846";
export const DANGER = "#bd271e";
/* Todo: sort remaining colors and investigate if we need them all (we could use `shadeColor()`) */
export const LIGHTEST_SUCCESS = "#e5ffed";
export const LIGHT_PRIMARY_COLOR = "#ed1556";
export const LIGHTEST_PRIMARY_COLOR = "#b2c6cf";
export const DARKEST_PRIMARY_COLOR = "#003f5f";
export const LIGHTER_PRIMARY = "#4d798f";
export const LIGHT_DANGER = "#d85ea3";
export const LIGHTEST_DANGER = "#e38bba";
export const DARK_SUCCESS_COLOR = "#025d00";
export const LIGHT_SUCCESS_COLOR = "#d0ffd9";
export const DARKER_PRIMARY = "#002f47";
export const GOLD = "#d4af37";
export const DARK_GOLD_COLOR = "#524217";
export const LIGHT_WARNING = "#ea7e66";
export const LIGHT_GOLD_COLOR = "#fdf6d4";
export const BACKGROUND_COLOR = "#f9f9f9";
export const LIGHTEST_GOLD = "#fdfde3";
export const MEDIUM_GREY_COLOR = "#a7b3b4";
export const DARK_GREY_COLOR = "#565656";
export const DARKEST_GREY = "#414141";
export const LIGHT_GREY_COLOR = "#c1cac9";
export const LIGHTER_GREY_COLOR = "#d4dada";
export const LIGHTEST_GREY = "#e5e8e8";
This folder contains a manifest file that holds the metainfo about the available modules and components in the custom
package.
An example that shows some extra pages and subscription detail plugins that are included from the custom folder:
```JSON
{
"name": "SURF",
"customPages": [
{
"name": "prefixes",
"path": "pages",
"file": "Prefixes",
"component": "Prefixes",
"showInMenu": true
},
{
"name": "tickets",
"path": "pages",
"file": "ServiceTickets",
"component": "ServiceTickets",
"showInMenu": true
},
{
"name": "tickets/create",
"path": "pages",
"file": "CreateServiceTicket",
"component": "CreateServiceTicket",
"showInMenu": false
}
],
"disabledRoutes": ["/YOUR_DISABLED_ROUTE_HERE"],
"disabledMenuItems": [],
"plugins": {
"subscriptionDetailPlugins": ["RenderDiagram", "RenderExternalLinks", "RenderDienstafname"]
}
}
```
import { BaseApiClient } from "api";
import { Organization, ServicePortFilterItem, ServicePortSubscription } from "utils/types";
abstract class CustomApiClientInterface extends BaseApiClient {
abstract portSubscriptions: (
tagList?: string[],
statusList?: string[],
productList?: string[]
) => Promise<ServicePortSubscription[]>;
abstract organisations: () => Promise<Organization[] | undefined>;
abstract locationCodes: () => Promise<string[] | undefined>;
abstract getPortSubscriptionsForNode: (id: string) => Promise<ServicePortFilterItem[]>;
}
export class CustomApiClient extends CustomApiClientInterface {
portSubscriptions = (...[,,]): Promise<ServicePortSubscription[]> => {
return this.fetchJson("non-existent-url");
};
organisations = async (): Promise<Organization[] | undefined> => {
return undefined;
};
locationCodes = async (): Promise<string[] | undefined> => {
return undefined;
};
getPortSubscriptionsForNode = (_: string): Promise<ServicePortFilterItem[]> => {
return this.fetchJson("non-existent-url");
};
}
/*
* Copyright 2019-2023 SURF.
* 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 { EuiButtonIcon, EuiCopy, EuiFlexGroup, EuiFlexItem } from "@elastic/eui";
import React, { useContext, useState } from "react";
import { FormattedMessage } from "react-intl";
import { useQuery } from "react-query";
import ApplicationContext from "utils/ApplicationContext";
function SubscriptionInstanceValueRow({
label,
value,
isSubscriptionValue,
isDeleted,
isExternalLinkValue,
toggleCollapsed,
type,
children,
}: React.PropsWithChildren<{
label: string;
value: string;
isSubscriptionValue: boolean;
isDeleted: boolean;
isExternalLinkValue: boolean;
toggleCollapsed: () => void;
type: string;
}>) {
const icon = children ? "minus" : "plus";
const { apiClient, theme } = useContext(ApplicationContext);
const { isLoading: subscriptionIsLoading, error: subscriptionError, data: subscriptionData } = useQuery(
["subscription", { id: isSubscriptionValue ? value : "disabled" }],
() => apiClient.subscriptionsDetailWithModel(value),
{
enabled: isSubscriptionValue,
}
);
const subscriptionLink =
isSubscriptionValue && !subscriptionIsLoading && !subscriptionError
? `${subscriptionData?.description} (${value})`
: value;
return (
<tbody className={theme}>
<tr>
<td>{label.toUpperCase()}</td>
<td colSpan={isDeleted ? 1 : 2}>
<div className="resource-type">
{isExternalLinkValue && !isDeleted && (
<i className={`fa fa-${icon}-circle`} onClick={toggleCollapsed} />
)}
{isSubscriptionValue && (
<EuiFlexGroup alignItems={"center"}>
<EuiFlexItem grow={false}>
<a target="_blank" rel="noopener noreferrer" href={`/subscriptions/${value}`}>
{subscriptionLink}
</a>
</EuiFlexItem>
<EuiCopy textToCopy={value}>
{(copy) => <EuiButtonIcon iconType={"copyClipboard"} onClick={copy} />}
</EuiCopy>
</EuiFlexGroup>
)}
{!isSubscriptionValue && <span>{value.toString()}</span>}
</div>
</td>
{isDeleted && (
<td>
<em className="error">
<FormattedMessage id={`subscription.${type}.removed`} />
</em>
</td>
)}
</tr>
{children && isExternalLinkValue && !isDeleted && (
<tr className="related-subscription">
<td className="whitespace" />
<td className="related-subscription-values" colSpan={2}>
{children}
</td>
</tr>
)}
</tbody>
);
}
interface IProps {
label: string;
value: string;
}
export default function SubscriptionInstanceValue({ label, value }: IProps) {
const [collapsed, setCollapsed] = useState(true);
const [data] = useState<any | null | undefined>(undefined);
const isSubscriptionValue = label.endsWith("subscription_id");
const isExternalLinkValue = false;
const isDeleted = isExternalLinkValue && data === null;
return (
<SubscriptionInstanceValueRow
label={label}
value={value}
isSubscriptionValue={isSubscriptionValue}
isDeleted={isDeleted}
isExternalLinkValue={isExternalLinkValue}
toggleCollapsed={() => setCollapsed(!collapsed)}
type={label}
>
{!!data && !collapsed && data}
</SubscriptionInstanceValueRow>
);
}
// eslint-disable-next-line no-template-curly-in-string
window.__env__.OPA_BUNDLE_URL = "${REACT_APP_OPA_BUNDLE_URL}";
{
"name": "standalone",
"customPages": [],
"disabledRoutes": ["non-existent-url"],
"disabledMenuItems": [],
"subscriptionDetailItems": [],
"plugins": {}
}
import NextAuth, { AuthOptions } from 'next-auth';
import { JWT } from 'next-auth/jwt';
import { OAuthConfig } from 'next-auth/providers';
import {
WfoSession,
WfoUserProfile,
} from '@orchestrator-ui/orchestrator-ui-components';
const token_endpoint_auth_method = process.env.NEXTAUTH_CLIENT_SECRET
? 'client_secret_basic'
: 'none';
const authActive = process.env.AUTH_ACTIVE?.toLowerCase() != 'false';
const wfoProvider: OAuthConfig<WfoUserProfile> = {
id: process.env.NEXTAUTH_ID || '',
name: process.env.NEXTAUTH_ID || '',
type: 'oauth',
clientId: process.env.NEXTAUTH_CLIENT_ID || '',
clientSecret: process.env.NEXTAUTH_CLIENT_SECRET || undefined,
wellKnown:
process.env.NEXTAUTH_WELL_KNOWN_OVERRIDE ??
`${process.env.NEXTAUTH_ISSUER || ''}/.well-known/openid-configuration`,
authorization: {
params: {
scope: 'openid profile email aarc',
},
},
idToken: true,
checks: ['pkce', 'state'],
userinfo: {
request: async (context) => {
const { client, tokens } = context;
if (!context.provider.wellKnown || !tokens.access_token) {
return {};
}
return await client.userinfo(tokens.access_token);
},
},
profile(profile) {
return {
id: profile.sub,
name: profile.name ?? profile.preferred_username,
email: profile.email,
};
},
client: {
token_endpoint_auth_method,
response_types: ['code'],
},
};
export const authOptions: AuthOptions = {
providers: authActive ? [wfoProvider] : [],
callbacks: {
async jwt({ token, account, profile }) {
// The "account" is only available right after signing in -- adding useful data to the token
if (account) {
token.accessToken = account.access_token;
token.profile = profile;
}
return token;
},
async session({ session, token }: { session: WfoSession; token: JWT }) {
// Assign data to the session to be available in the client through the useSession hook
session.profile = token.profile as WfoUserProfile | undefined;
session.accessToken = token.accessToken
? String(token.accessToken)
: '';
return session;
},
},
};
export default NextAuth(authOptions);
plugin folder
import {
AcceptField,
BoolField,
DateField,
DividerField,
LabelField,
ListField,
LocationCodeField,
LongTextField,
NestField,
NumField,
OptGroupField,
OrganisationField,
ProductField,
RadioField,
SelectField,
SubscriptionField,
SubscriptionSummaryField,
SummaryField,
TextField,
} from "lib/uniforms-surfnet/src";
import { Context, GuaranteedProps } from "uniforms";
import { AutoField } from "uniforms-unstyled";
export function autoFieldFunction(props: GuaranteedProps<unknown> & Record<string, any>, uniforms: Context<unknown>) {
const { allowedValues, checkboxes, fieldType, field } = props;
const { format } = field;
switch (fieldType) {
case Number:
// If you need custom field for numeric resource types add a switch here
break;
case Object:
switch (format) {
case "optGroup":
return OptGroupField;
}
break;
case String:
switch (format) {
case "subscriptionId":
return SubscriptionField;
case "productId":
return ProductField;
case "locationCode":
return LocationCodeField;
case "organisationId":
return OrganisationField;
case "long":
return LongTextField;
case "label":
return LabelField;
case "divider":
return DividerField;
case "summary":
return SummaryField;
case "subscription":
return SubscriptionSummaryField;
case "accept":
return AcceptField;
}
break;
}
if (allowedValues && format !== "accept") {
if (checkboxes && fieldType !== Array) {
return RadioField;
} else {
return SelectField;
}
} else {
switch (fieldType) {
case Array:
return ListField;
case Boolean:
return BoolField;
case Date:
return DateField;
case Number:
return NumField;
case Object:
return NestField;
case String:
return TextField;
}
}
// Todo React upgrade: fix uniform types
// @ts-ignore
return AutoField.defaultComponentDetector(props, uniforms);
}
import { loadPolicy } from "@open-policy-agent/opa-wasm";
declare global {
interface Window {
__env__: {
OPA_BUNDLE_URL?: string;
};
}
}
export async function createPolicyCheck(user?: Partial<Oidc.Profile>) {
if (!user) {
return () => true;
}
const opaBundletUrl = window.__env__?.OPA_BUNDLE_URL;
if (typeof opaBundletUrl === 'undefined') {
console.log('OPA_BUNDLE_URL is not defined');
return () => true;
}
const policyResult = await fetch(opaBundletUrl);
const policyWasm = await policyResult.arrayBuffer();
try {
const policy = await loadPolicy(policyWasm);
function allowed(resource: string): boolean {
const input: any = {
resource: resource,
method: "GET",
...user,
};
const resultSet = policy.evaluate(input);
if (resultSet == null || resultSet.length === 0) {
console.error("evaluation error", resultSet);
return false;
}
return resultSet[0].result;
}
return allowed;
} catch {
console.error("policy evaluation error");
return (_: string) => false;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment