Skip to content

Commit

Permalink
feat: add project dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
andre-code committed Jan 25, 2023
1 parent 1f20402 commit 4da97dc
Show file tree
Hide file tree
Showing 30 changed files with 867 additions and 443 deletions.
15 changes: 15 additions & 0 deletions client/public/explore.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions client/public/frame.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 4 additions & 8 deletions client/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ import { Helmet } from "react-helmet";
import { Route, Switch } from "react-router-dom";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import { useSelector } from "react-redux";

import Project from "./project/Project";
import { ProjectList } from "./project/list";
import { NewProject } from "./project/new";
import DatasetList from "./dataset/list/DatasetList.container";
import { AnonymousHome, Landing, RenkuNavBar, FooterNavbar } from "./landing";
import { AnonymousHome, RenkuNavBar, FooterNavbar } from "./landing";
import { Notebooks } from "./notebooks";
import { Login, LoginHelper } from "./authentication";
import Help from "./help";
Expand All @@ -52,7 +53,7 @@ import AppContext from "./utils/context/appContext";
import { setupWebSocket } from "./websocket";
import SearchPage from "./features/kgSearch/KgSearchPage";
import InactiveKGProjectsPage from "./features/inactiveKgProjects/InactiveKgProjects";
import { useSelector } from "react-redux";
import Dashboard from "./features/dashboard/Dashboard";

export const ContainerWrap = ({ children, fullSize = false }) => {
const classContainer = !fullSize ? "container-xxl py-4 mt-2 renku-container" : "w-100";
Expand Down Expand Up @@ -91,12 +92,7 @@ function CentralContentContainer(props) {
<Route exact path={Url.get(Url.pages.landing)} render={
p => (props.user.logged) ?
<ContainerWrap>
<Landing.Home
key="landing" welcomePage={props.params["WELCOME_PAGE"]}
user={props.user}
client={props.client}
model={props.model}
{...p} />
<Dashboard />
</ContainerWrap> : null
} />
<Route path={Url.get(Url.pages.help)} render={
Expand Down
53 changes: 53 additions & 0 deletions client/src/features/dashboard/Dashboard.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
.rk-dashboard {
h1{
font-size: 24px;
margin-bottom: 30px;
}
h3{
font-size: 20px;
}
.rk-dashboard-title {
font-size: 27px;
font-weight: bold;
}
.rk-dashboard-project {
background-color: white;
padding-top: 20px;
padding-bottom: 20px;
}
.rk-dashboard-project {
padding-right: 23px;
padding-left: 23px;
}
.rk-dashboard-section-header {
padding-right: 0;
padding-left: 0;
}
.rk-dashboard-link--text {
display: inline-block;
}

@media (max-width: 650px) {
h1{
font-size: 20px;
margin-bottom: 30px;
}
h3{
font-size: 18px;
}
.rk-dashboard-title {
font-size: 24px;
}
.rk-dashboard-project {
padding-right: 0;
padding-left: 0;
}
.rk-dashboard-section-header {
padding-right: 15px;
padding-left: 15px;
}
.rk-dashboard-link--text {
display: none;
}
}
}
19 changes: 19 additions & 0 deletions client/src/features/dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { RootStateOrAny, useSelector } from "react-redux";
import React from "react";
import { ProjectsDashboard } from "./components/ProjectsDashboard";
import "./Dashboard.scss";
import ProjectsInactiveKGWarning from "./components/InactiveKgProjects";

function Dashboard() {
const user = useSelector( (state: RootStateOrAny) => state.stateModel.user);

return (
<div className="rk-dashboard">
<h1 data-cy="dashboard-title">Renku Dashboard - {user.data.name}</h1>
<ProjectsInactiveKGWarning />
<ProjectsDashboard />
</div>
);
}

export default Dashboard;
56 changes: 56 additions & 0 deletions client/src/features/dashboard/components/InactiveKgProjects.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*!
* Copyright 2023 - Swiss Data Science Center (SDSC)
* A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
* Eidgenössische Technische Hochschule Zürich (ETHZ).
*
* 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 { RootStateOrAny, useSelector } from "react-redux";
import { useInactiveProjectSelector } from "../../inactiveKgProjects/inactiveKgProjectsSlice";
import useGetInactiveProjects from "../../../utils/customHooks/UseGetInactiveProjects";
import { WarnAlert } from "../../../utils/components/Alert";
import { Link } from "react-router-dom";
import React from "react";

export function ProjectsInactiveKGWarning() {
const user = useSelector((state: RootStateOrAny) => state.stateModel.user);
const projectList = useInactiveProjectSelector(
(state) => state.kgInactiveProjects
);
const { data, isFetching, isLoading } = useGetInactiveProjects(user?.data?.id);

if (!user.logged)
return null;

if (isLoading || isFetching || data?.length === 0)
return null;

let totalProjects;
if (projectList.length > 0) {
totalProjects = projectList.filter( p => p.progressActivation !== 100).length;
if (totalProjects === 0)
return null;
}
else {
totalProjects = data?.length;
}

return <WarnAlert>
<div data-cy="inactive-kg-project-alert">
You have {totalProjects} projects that are not in the Knowledge Graph.{" "}
<Link to="/inactive-kg-projects">Activate your projects</Link> to make them searchable on Renku.
</div>
</WarnAlert>;
}

export default ProjectsInactiveKGWarning;
184 changes: 184 additions & 0 deletions client/src/features/dashboard/components/ProjectsDashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*!
* Copyright 2023 - Swiss Data Science Center (SDSC)
* A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
* Eidgenössische Technische Hochschule Zürich (ETHZ).
*
* 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 { SearchEntitiesQueryParams, useSearchEntitiesQuery } from "../../kgSearch/KgSearchApi";
import { SortingOptions } from "../../../utils/components/sortingEntities/SortingEntities";
import { InfoAlert } from "../../../utils/components/Alert";
import React, { Fragment } from "react";
import { Docs } from "../../../utils/constants/Docs";
import { ExternalLink } from "../../../utils/components/ExternalLinks";
import { Link, useHistory } from "react-router-dom";
import { urlMap } from "../../../project/list/ProjectList.container";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { Url } from "../../../utils/helpers/url";
import { MarkdownTextExcerpt } from "../../../utils/components/markdown/RenkuMarkdown";
import ListDisplay from "../../../utils/components/List";
import { Loader } from "../../../utils/components/Loader";
import { useDispatch } from "react-redux";
import { setAuthor } from "../../kgSearch/KgSearchSlice";
import { KgAuthor } from "../../kgSearch/KgSearch";
import { Button } from "../../../utils/ts-wrappers";
import useGetRecentlyVisitedProjects from "../../../utils/customHooks/useGetRecentlyVisitedProjects";

interface ProjectAlertProps {
total?: number;
}
function ProjectAlert({ total }: ProjectAlertProps) {
if (total === undefined)
return null;

return total === 0 ?
<InfoAlert timeout={0}>
<div data-cy="project-alert" className="mb-0" style={{ textAlign: "justify" }}>
<h3><strong>You don’t have any project yet.</strong></h3>
<p>If you are here for your first time, we recommend you go to through our{" "}
<ExternalLink role="text" title="tutorial" className="fw-bold"
url={Docs.READ_THE_DOCS_TUTORIALS_STARTING} />.{" "}
You can also <ExternalLink role="text" title="create a new project"
url="/projects/new" className="fw-bold" />,{" "}
<ExternalLink role="text" title="explore other projects" url="/search" className="fw-bold" /> or {" "}
<ExternalLink role="text" title="search" url="/search" className="fw-bold" />{" "}
for a specific project or dataset.</p>
</div>
</InfoAlert> : null;
}

interface OtherProjectsButtonProps {
totalOwnProjects: number;
}
function OtherProjectsButton({ totalOwnProjects }: OtherProjectsButtonProps) {
const dispatch = useDispatch();
const history = useHistory();
const handleOnClick = (e: React.MouseEvent<HTMLElement>, author: KgAuthor) => {
e.preventDefault();
dispatch(setAuthor(author));
history.push(Url.get(Url.pages.searchEntities));
};
return totalOwnProjects > 0 ?
(
<div className="d-flex justify-content-center">
<Button data-cy="view-my-projects-btn" className="btn btn-outline-rk-green"
onClick={(e: React.MouseEvent<HTMLElement>) => handleOnClick(e, "user")}>
<div className="d-flex gap-2 text-rk-green">
<img src="/frame.svg" className="rk-icon rk-icon-md" />View all my Projects</div>
</Button></div>
) :
(<div className="d-flex justify-content-center">
<Button data-cy="explore-other-projects-btn" className="btn btn-outline-rk-green"
onClick={(e: React.MouseEvent<HTMLElement>) => handleOnClick(e, "all")}>
<div className="d-flex gap-2 text-rk-green">
<img src="/explore.svg" className="rk-icon rk-icon-md" />Explore other Projects</div>
</Button></div>);
}

interface ProjectListProps {
projects: any [];
gridDisplay: boolean;
}
function ProjectListRows({ projects, gridDisplay }: ProjectListProps) {
const projectItems = projects.map(project => {
const namespace = project.namespace ? project.namespace.full_path : "";
const path = project.path;
const url = Url.get(Url.pages.project, { namespace, path });
return {
id: project.id,
url: url,
itemType: "project",
title: project.name,
creators: project.owner ? [project.owner] : [project.namespace],
slug: project.path_with_namespace,
description: project.description ?
<Fragment>
<MarkdownTextExcerpt markdownText={project.description} singleLine={!gridDisplay}
charsLimit={gridDisplay ? 200 : 150} />
<span className="ms-1">{project.description.includes("\n") ? " [...]" : ""}</span>
</Fragment>
: " ",
tagList: project.tag_list,
timeCaption: project.last_activity_at,
imageUrl: project.avatar_url,
visibility: project.visibility
};
});

return <Fragment>
<ListDisplay
itemsType="project"
search={null}
currentPage={null}
gridDisplay={gridDisplay}
totalItems={projectItems.length}
perPage={projectItems.length}
items={projectItems}
gridColumnsBreakPoint={{
default: 2,
1100: 2,
700: 2,
500: 1
}}
/>
</Fragment>;
}

const TOTAL_RECENTLY_VISITED_PROJECT = 5;
function ProjectsDashboard() {
const searchRequest: SearchEntitiesQueryParams = {
phrase: "",
sort: SortingOptions.DescMatchingScore,
page: 1,
perPage: 50,
author: "user",
type: {
project: true,
dataset: false,
}
};
const { data, isFetching, isLoading, error } = useSearchEntitiesQuery(searchRequest);
const { projects, isFetchingProjects } = useGetRecentlyVisitedProjects(TOTAL_RECENTLY_VISITED_PROJECT);
const totalUserProjects = isFetching || isLoading || !data || error ? undefined : data.total;
let projectsToShow;
if (isFetchingProjects) {
projectsToShow = <Loader />;
}
else {
projectsToShow = projects?.length > 0 ?
<ProjectListRows projects={projects} gridDisplay={false} />
: <p className="rk-dashboard-section-header">You have no current project yet</p>;
}
const otherProjectsBtn = totalUserProjects === undefined ? null :
<OtherProjectsButton totalOwnProjects={totalUserProjects} />;
return (
<>
<ProjectAlert total={totalUserProjects} />
<div className="rk-dashboard-project" data-cy="projects-container">
<div className="rk-dashboard-section-header d-flex justify-content-between align-items-center flex-wrap">
<h3 className="rk-dashboard-title" key="project-header">Projects</h3>
<Link className="btn btn-secondary btn-icon-text rk-dashboard-link" role="button" to={urlMap.projectNewUrl}>
<FontAwesomeIcon icon={faPlus} />
<span className="rk-dashboard-link--text">Create a new project</span>
</Link>
</div>
{projectsToShow}
{otherProjectsBtn}
</div>
</>
);

}

export { ProjectsDashboard };
2 changes: 1 addition & 1 deletion client/src/features/kgSearch/KgSearchApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { VisibilitiesFilter } from "../../utils/components/visibilityFilter/Visi
import { TypeEntitySelection } from "../../utils/components/typeEntityFilter/TypeEntityFilter";
import { SortingOptions } from "../../utils/components/sortingEntities/SortingEntities";

type SearchEntitiesQueryParams = {
export type SearchEntitiesQueryParams = {
phrase: string;
sort: SortingOptions;
page: number;
Expand Down
Loading

0 comments on commit 4da97dc

Please sign in to comment.