| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563 |
- import React, { Component } from "react";
- import { Route, RouteComponentProps, Switch, withRouter } from "react-router";
- import styled from "styled-components";
- import api from "shared/api";
- import { H } from "highlight.run";
- import { Context } from "shared/Context";
- import { PorterUrl, pushFiltered, pushQueryParams } from "shared/routing";
- import { ClusterType, ProjectType } from "shared/types";
- import ConfirmOverlay from "components/ConfirmOverlay";
- import Loading from "components/Loading";
- import ClusterDashboard from "./cluster-dashboard/ClusterDashboard";
- import Dashboard from "./dashboard/Dashboard";
- import WelcomeForm from "./WelcomeForm";
- import Integrations from "./integrations/Integrations";
- import Templates from "./launch/Launch";
- import Navbar from "./navbar/Navbar";
- import ProjectSettings from "./project-settings/ProjectSettings";
- import Sidebar from "./sidebar/Sidebar";
- import PageNotFound from "components/PageNotFound";
- import { fakeGuardedRoute } from "shared/auth/RouteGuard";
- import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
- import discordLogo from "../../assets/discord.svg";
- import Onboarding from "./onboarding/Onboarding";
- import ModalHandler from "./ModalHandler";
- import { NewProjectFC } from "./new-project/NewProject";
- // Guarded components
- const GuardedProjectSettings = fakeGuardedRoute("settings", "", [
- "get",
- "list",
- "update",
- "create",
- "delete",
- ])(ProjectSettings);
- const GuardedIntegrations = fakeGuardedRoute("integrations", "", [
- "get",
- "list",
- "update",
- "create",
- "delete",
- ])(Integrations);
- type PropsType = RouteComponentProps &
- WithAuthProps & {
- logOut: () => void;
- currentProject: ProjectType;
- currentCluster: ClusterType;
- currentRoute: PorterUrl;
- };
- type StateType = {
- forceSidebar: boolean;
- showWelcome: boolean;
- handleDO: boolean; // Trigger DO infra calls after oauth flow if needed
- ghRedirect: boolean;
- forceRefreshClusters: boolean; // For updating ClusterSection from modal on deletion
- // Track last project id for refreshing clusters on project change
- prevProjectId: number | null;
- showWelcomeForm: boolean;
- };
- // TODO: Handle cluster connected but with some failed infras (no successful set)
- // TODO: Set up current view / sidebar tab as dynamic Routes
- class Home extends Component<PropsType, StateType> {
- state = {
- forceSidebar: true,
- showWelcome: false,
- prevProjectId: null as number | null,
- forceRefreshClusters: false,
- sidebarReady: false,
- handleDO: false,
- ghRedirect: false,
- showWelcomeForm: true,
- };
- getMetadata = () => {
- api
- .getMetadata("<token>", {}, {})
- .then((res) => {
- this.context.setCapabilities(res.data);
- })
- .catch((err) => {
- console.log(err);
- });
- };
- getProjects = (id?: number) => {
- let { user, setProjects, setCurrentProject } = this.context;
- let { currentProject } = this.props;
- let queryString = window.location.search;
- let urlParams = new URLSearchParams(queryString);
- let projectId = urlParams.get("project_id");
- if (!projectId && currentProject?.id) {
- pushQueryParams(this.props, { project_id: currentProject.id.toString() });
- }
- api
- .getProjects("<token>", {}, { id: user.userId })
- .then((res) => {
- if (res.data) {
- if (res.data.length === 0) {
- this.redirectToNewProject();
- } else if (res.data.length > 0 && !currentProject) {
- setProjects(res.data);
- let foundProject = null;
- if (id) {
- res.data.forEach((project: ProjectType, i: number) => {
- if (project.id === id) {
- foundProject = project;
- }
- });
- setCurrentProject(foundProject || res.data[0]);
- }
- if (!foundProject) {
- res.data.forEach((project: ProjectType, i: number) => {
- if (
- project.id.toString() ===
- localStorage.getItem("currentProject")
- ) {
- foundProject = project;
- }
- });
- setCurrentProject(foundProject || res.data[0]);
- }
- }
- }
- })
- .catch(console.log);
- };
- componentDidMount() {
- this.checkOnboarding();
- let { match } = this.props;
- let { user } = this.context;
- // Initialize Highlight
- if (
- window.location.href.includes("dashboard.getporter.dev") &&
- !user.email.includes("@getporter.dev")
- ) {
- H.init("y2d13lgr");
- H.identify(user.email, { id: user.id });
- }
- // Handle redirect from DO
- let queryString = window.location.search;
- let urlParams = new URLSearchParams(queryString);
- let err = urlParams.get("error");
- if (err) {
- this.context.setCurrentError(err);
- }
- let defaultProjectId = parseInt(urlParams.get("project_id"));
- this.setState({ ghRedirect: urlParams.get("gh_oauth") !== null });
- urlParams.delete("gh_oauth");
- this.getProjects(defaultProjectId);
- this.getMetadata();
- if (
- !this.context.hasFinishedOnboarding &&
- this.props.history.location.pathname &&
- !this.props.history.location.pathname.includes("onboarding")
- ) {
- this.context.setCurrentModal("RedirectToOnboardingModal");
- }
- }
- async checkIfProjectHasBilling(projectId: number) {
- if (!projectId) {
- return false;
- }
- try {
- const res = await api.getHasBilling(
- "<token>",
- {},
- { project_id: projectId }
- );
- this.context.setHasBillingEnabled(res.data?.has_billing);
- return res?.data?.has_billing;
- } catch (error) {
- console.log(error);
- }
- }
- async checkOnboarding() {
- try {
- const project_id = this.context?.currentProject?.id;
- if (!project_id) {
- return;
- }
- const res = await api.getOnboardingState("<token>", {}, { project_id });
- if (res.status === 404) {
- this.context.setHasFinishedOnboarding(true);
- return;
- }
- if (res?.data && res?.data.current_step !== "clean_up") {
- this.context.setHasFinishedOnboarding(false);
- } else {
- this.context.setHasFinishedOnboarding(true);
- }
- } catch (error) {}
- }
- // TODO: Need to handle the following cases. Do a deep rearchitecture (Prov -> Dashboard?) if need be:
- // 1. Make sure clicking cluster in drawer shows cluster-dashboard
- // 2. Make sure switching projects shows appropriate initial view (dashboard || provisioner)
- // 3. Make sure initializing from URL (DO oauth) displays the appropriate initial view
- componentDidUpdate(prevProps: PropsType) {
- if (
- !this.context.hasFinishedOnboarding &&
- prevProps.match.url !== this.props.match.url &&
- this.props.history.location.pathname &&
- !this.props.history.location.pathname.includes("onboarding") &&
- !this.props.history.location.pathname.includes("new-project") &&
- !this.props.history.location.pathname.includes("project-settings")
- ) {
- this.context.setCurrentModal("RedirectToOnboardingModal");
- }
- if (prevProps.currentProject?.id !== this.props.currentProject?.id) {
- this.checkOnboarding();
- this.checkIfProjectHasBilling(this?.context?.currentProject?.id)
- .then((isBillingEnabled) => {
- if (isBillingEnabled) {
- api
- .getUsage(
- "<token>",
- {},
- { project_id: this.context?.currentProject?.id }
- )
- .then((res) => {
- const usage = res.data;
- this.context.setUsage(usage);
- if (usage.exceeded) {
- this.context.setCurrentModal("UsageWarningModal", {
- usage,
- });
- }
- })
- .catch(console.log);
- }
- })
- .catch(console.log);
- }
- if (
- prevProps.currentProject !== this.props.currentProject ||
- (!prevProps.currentCluster && this.props.currentCluster)
- ) {
- this.getMetadata();
- }
- }
- projectOverlayCall = async () => {
- let { user, setProjects, setCurrentProject } = this.context;
- try {
- const res = await api.getProjects("<token>", {}, { id: user.userId });
- if (!res.data) {
- this.context.setCurrentModal(null, null);
- return;
- }
- setProjects(res.data);
- if (!res.data.length) {
- setCurrentProject(null, () => this.redirectToNewProject());
- } else {
- setCurrentProject(res.data[0]);
- }
- this.context.setCurrentModal(null, null);
- } catch (error) {
- /** @todo Centralize with error handler */
- console.log(error);
- }
- };
- handleDelete = async () => {
- let { setCurrentModal, currentProject } = this.context;
- localStorage.removeItem(currentProject.id + "-cluster");
- try {
- await api.deleteProject("<token>", {}, { id: currentProject?.id });
- this.projectOverlayCall();
- } catch (error) {
- /** @todo Centralize with error handler */
- console.log(error);
- }
- try {
- const res = await api.getClusters<
- {
- infra_id?: number;
- name: string;
- }[]
- >("<token>", {}, { id: currentProject?.id });
- const destroyInfraPromises = res.data.map((cluster) => {
- if (!cluster.infra_id) {
- return undefined;
- }
- return api.destroyInfra(
- "<token>",
- { name: cluster.name },
- { project_id: currentProject.id, infra_id: cluster.infra_id }
- );
- });
- await Promise.all(destroyInfraPromises);
- } catch (error) {
- console.log(error);
- }
- setCurrentModal(null, null);
- pushFiltered(this.props, "/dashboard", []);
- };
- redirectToNewProject = () => {
- pushFiltered(this.props, "/new-project", ["project_id"]);
- };
- redirectToOnboarding = () => {
- pushFiltered(this.props, "/onboarding", []);
- };
- render() {
- let {
- currentModal,
- setCurrentModal,
- currentProject,
- currentOverlay,
- projects,
- } = this.context;
- const { cluster, baseRoute } = this.props.match.params as any;
- return (
- <StyledHome>
- <ModalHandler
- setRefreshClusters={(x) => this.setState({ forceRefreshClusters: x })}
- />
- {currentOverlay && (
- <ConfirmOverlay
- show={true}
- message={currentOverlay.message}
- onYes={currentOverlay.onYes}
- onNo={currentOverlay.onNo}
- />
- )}
- {/* Render sidebar when there's at least one project */}
- {projects?.length > 0 && baseRoute !== "new-project" ? (
- <Sidebar
- key="sidebar"
- forceSidebar={this.state.forceSidebar}
- setWelcome={(x: boolean) => this.setState({ showWelcome: x })}
- currentView={this.props.currentRoute}
- forceRefreshClusters={this.state.forceRefreshClusters}
- setRefreshClusters={(x: boolean) =>
- this.setState({ forceRefreshClusters: x })
- }
- />
- ) : (
- <>
- <DiscordButton href="https://discord.gg/34n7NN7FJ7" target="_blank">
- <Icon src={discordLogo} />
- Join Our Discord
- </DiscordButton>
- {/* This should only be shown on the first render of the app */}
- {this.state.showWelcomeForm &&
- localStorage.getItem("welcomed") != "true" &&
- projects?.length === 0 && (
- <>
- <WelcomeForm
- closeForm={() => this.setState({ showWelcomeForm: false })}
- />
- <Navbar
- logOut={this.props.logOut}
- currentView={this.props.currentRoute} // For form feedback
- />
- </>
- )}
- </>
- )}
- <ViewWrapper id="HomeViewWrapper">
- <Navbar
- logOut={this.props.logOut}
- currentView={this.props.currentRoute} // For form feedback
- />
- <Switch>
- <Route
- path="/new-project"
- render={() => {
- return <NewProjectFC />;
- }}
- ></Route>
- <Route
- path="/onboarding"
- render={() => {
- return <Onboarding />;
- }}
- />
- <Route
- path="/dashboard"
- render={() => {
- return (
- <DashboardWrapper>
- <Dashboard
- projectId={this.context.currentProject?.id}
- setRefreshClusters={(x: boolean) =>
- this.setState({ forceRefreshClusters: x })
- }
- />
- </DashboardWrapper>
- );
- }}
- />
- <Route
- path={[
- "/cluster-dashboard",
- "/applications",
- "/jobs",
- "/env-groups",
- ]}
- render={() => {
- let { currentCluster } = this.context;
- if (currentCluster?.id === -1) {
- return <Loading />;
- } else if (!currentCluster || !currentCluster.name) {
- return (
- <DashboardWrapper>
- <PageNotFound />
- </DashboardWrapper>
- );
- }
- return (
- <DashboardWrapper>
- <ClusterDashboard
- currentCluster={currentCluster}
- setSidebar={(x: boolean) =>
- this.setState({ forceSidebar: x })
- }
- currentView={this.props.currentRoute}
- // setCurrentView={(x: string) => this.setState({ currentView: x })}
- />
- </DashboardWrapper>
- );
- }}
- />
- <Route
- path={"/integrations"}
- render={() => <GuardedIntegrations />}
- />
- <Route
- path={"/project-settings"}
- render={() => <GuardedProjectSettings />}
- />
- <Route path={"*"} render={() => <Templates />} />
- </Switch>
- </ViewWrapper>
- <ConfirmOverlay
- show={currentModal === "UpdateProjectModal"}
- message={
- currentProject
- ? `Are you sure you want to delete ${currentProject.name}?`
- : ""
- }
- onYes={this.handleDelete}
- onNo={() => setCurrentModal(null, null)}
- />
- </StyledHome>
- );
- }
- }
- Home.contextType = Context;
- export default withRouter(withAuth(Home));
- const ViewWrapper = styled.div`
- height: 100%;
- width: 100vw;
- padding-top: 10vh;
- overflow-y: auto;
- display: flex;
- flex: 1;
- justify-content: center;
- background: #202227;
- position: relative;
- `;
- const DashboardWrapper = styled.div`
- width: calc(85%);
- min-width: 300px;
- `;
- const StyledHome = styled.div`
- width: 100vw;
- height: 100vh;
- position: fixed;
- top: 0;
- left: 0;
- margin: 0;
- user-select: none;
- display: flex;
- justify-content: center;
- @keyframes floatInModal {
- from {
- opacity: 0;
- transform: translateY(30px);
- }
- to {
- opacity: 1;
- transform: translateY(0px);
- }
- }
- `;
- const DiscordButton = styled.a`
- position: absolute;
- z-index: 1;
- text-decoration: none;
- bottom: 17px;
- display: flex;
- align-items: center;
- width: 170px;
- left: 15px;
- border: 2px solid #ffffff44;
- border-radius: 3px;
- color: #ffffff44;
- height: 40px;
- font-family: Work Sans, sans-serif;
- font-size: 14px;
- font-weight: bold;
- cursor: pointer;
- :hover {
- > img {
- opacity: 60%;
- }
- color: #ffffff88;
- border-color: #ffffff88;
- }
- `;
- const Icon = styled.img`
- height: 25px;
- width: 25px;
- opacity: 30%;
- margin-left: 7px;
- margin-right: 5px;
- `;
|