Skip to content

Commit

Permalink
feat: DIA-1794: [FE] Implement home page in LSO and LSE (#6885)
Browse files Browse the repository at this point in the history
Co-authored-by: bmartel <[email protected]>
Co-authored-by: yyassi-heartex <[email protected]>
Co-authored-by: hlomzik <[email protected]>
Co-authored-by: MihajloHoma <[email protected]>
Co-authored-by: borisheartex <[email protected]>
  • Loading branch information
6 people authored Jan 31, 2025
1 parent 2ab32ed commit e95280e
Show file tree
Hide file tree
Showing 58 changed files with 1,102 additions and 173 deletions.
2 changes: 1 addition & 1 deletion Dockerfile.development
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
ARG NODE_VERSION=18
ARG PYTHON_VERSION=3.12
ARG POETRY_VERSION=1.8.4
ARG POETRY_VERSION=2.0.1
ARG VERSION_OVERRIDE
ARG BRANCH_OVERRIDE

Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ docker-run-dev:
docker-migrate-dev:
docker compose run app python3 /label-studio/label_studio/manage.py migrate

docker-collectstatic-dev:
docker compose run app python3 /label-studio/label_studio/manage.py collectstatic

# Install modules
frontend-install:
cd web && yarn install --frozen-lockfile;
Expand Down
3 changes: 3 additions & 0 deletions label_studio/core/templates/home/home.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
9 changes: 6 additions & 3 deletions label_studio/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import pandas as pd
import requests
from core import utils
from core.feature_flags import all_flags, get_feature_file_path
from core.feature_flags import all_flags, flag_set, get_feature_file_path
from core.label_config import generate_time_series_json
from core.utils.common import collect_versions
from core.utils.io import find_file
Expand All @@ -27,7 +27,7 @@
HttpResponseServerError,
JsonResponse,
)
from django.shortcuts import redirect, reverse
from django.shortcuts import redirect, render, reverse
from django.template import loader
from django.utils._os import safe_join
from django.views.decorators.csrf import csrf_exempt
Expand Down Expand Up @@ -55,7 +55,10 @@ def main(request):
return redirect(reverse('user-login'))

# business mode access
return redirect(reverse('projects:project-index'))
if flag_set('fflag_all_feat_dia_1777_ls_homepage_short'):
return render(request, 'home/home.html')
else:
return redirect(reverse('projects:project-index'))

# not authenticated
return redirect(reverse('user-login'))
Expand Down
10 changes: 8 additions & 2 deletions label_studio/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ def user_signup(request):

# checks if the URL is a safe redirection.
if not next_page or not url_has_allowed_host_and_scheme(url=next_page, allowed_hosts=request.get_host()):
next_page = reverse('projects:project-index')
if flag_set('fflag_all_feat_dia_1777_ls_homepage_short'):
next_page = reverse('main')
else:
next_page = reverse('projects:project-index')

user_form = forms.UserSignupForm()
organization_form = OrganizationSignupForm()
Expand Down Expand Up @@ -101,7 +104,10 @@ def user_login(request):

# checks if the URL is a safe redirection.
if not next_page or not url_has_allowed_host_and_scheme(url=next_page, allowed_hosts=request.get_host()):
next_page = reverse('projects:project-index')
if flag_set('fflag_all_feat_dia_1777_ls_homepage_short'):
next_page = reverse('main')
else:
next_page = reverse('projects:project-index')

login_form = load_func(settings.USER_LOGIN_FORM)
form = login_form()
Expand Down
1 change: 1 addition & 0 deletions web/.stylelintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"selector-class-pattern": null,
"custom-property-pattern": null,
"no-descending-specificity": null,
"function-no-unknown": null,
"scss/no-global-function-names": null,
"scss/function-no-unknown": null,
"selector-pseudo-class-no-unknown": [
Expand Down
11 changes: 11 additions & 0 deletions web/apps/labelstudio/src/app/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,18 @@ import { FF_OPTIC_2, FF_UNSAVED_CHANGES, FF_PRODUCT_TOUR, isFF } from "../utils/
import { TourProvider } from "@humansignal/core";
import { ToastProvider, ToastViewport } from "@humansignal/ui";
import "@humansignal/ui/src/tailwind.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { JotaiProvider, JotaiStore } from "../utils/jotai-store";

const baseURL = new URL(APP_SETTINGS.hostname || location.origin);
export const UNBLOCK_HISTORY_MESSAGE = "UNBLOCK_HISTORY";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});

const browserHistory = createBrowserHistory({
basename: baseURL.pathname || "/",
Expand Down Expand Up @@ -57,6 +66,8 @@ const App = ({ content }) => {
<Router history={browserHistory}>
<MultiProvider
providers={[
<JotaiProvider key="jotai" store={JotaiStore} />,
<QueryClientProvider key="query" client={queryClient} />,
<AppStoreProvider key="app-store" />,
<ApiProvider key="api" />,
<ConfigProvider key="config" />,
Expand Down
4 changes: 3 additions & 1 deletion web/apps/labelstudio/src/components/Menubar/Menubar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import "./Menubar.scss";
import "./MenuContent.scss";
import "./MenuSidebar.scss";
import { ModelsPage } from "../../pages/Organization/Models/ModelsPage";
import { FF_DIA_835, isFF } from "../../utils/feature-flags";
import { FF_DIA_835, FF_HOMEPAGE, isFF } from "../../utils/feature-flags";
import { IconHome } from "@humansignal/ui";

export const MenubarContext = createContext();

Expand Down Expand Up @@ -183,6 +184,7 @@ export const Menubar = ({ enabled, defaultOpened, defaultPinned, children, onSid
style={{ width: 240 }}
>
<Menu>
{isFF(FF_HOMEPAGE) && <Menu.Item label="Home" to="/" icon={<IconHome />} data-external exact />}
<Menu.Item label="Projects" to="/projects" icon={<IconFolder />} data-external exact />
<Menu.Item label="Organization" to="/organization" icon={<IconPersonInCircle />} data-external exact />
{isFF(FF_DIA_835) && <Menu.Item label="Models" to={ModelsPage.path} icon={<IconModel />} exact />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const ProjectName = ({ name, setName, onSaveName, onSubmit, error, description,
</form>
);

export const CreateProject = ({ onClose }) => {
export const CreateProject = ({ onClose, redirect = true }) => {
const [step, _setStep] = React.useState("name"); // name | import | config
const [waiting, setWaitingStatus] = React.useState(false);

Expand Down Expand Up @@ -173,9 +173,9 @@ export const CreateProject = ({ onClose }) => {
},
});
setWaitingStatus(false);
history.replace("/projects");
redirect && history.replace("/projects");
onClose?.();
}, [project]);
}, [project, redirect]);

return (
<Modal onHide={onDelete} closeOnClickOutside={false} allowToInterceptEscape fullscreen visible bare>
Expand Down
213 changes: 213 additions & 0 deletions web/apps/labelstudio/src/pages/Home/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import type { Page } from "../types/Page";
import { Button } from "@humansignal/shad/components/ui/button";
import { IconFolder, SimpleCard, Spinner } from "@humansignal/ui";
import { IconExternal, IconFolderAdd, IconHumanSignal, IconUserAdd } from "@humansignal/icons";
import { HeidiTips } from "../../components/HeidiTips/HeidiTips";
import { useQuery } from "@tanstack/react-query";
import { useAPI } from "../../providers/ApiProvider";
import { useState } from "react";
import { CreateProject } from "../CreateProject/CreateProject";
import { InviteLink } from "../Organization/PeoplePage/InviteLink";
import { Heading, Sub } from "@humansignal/typography";

const PROJECTS_TO_SHOW = 10;

const resources = [
{
title: "Documentation",
url: "https://labelstud.io/guide/",
},
{
title: "API Documentation",
url: "https://api.labelstud.io/api-reference/introduction/getting-started",
},
{
title: "Release Notes",
url: "https://labelstud.io/learn/categories/release-notes/",
},
{
title: "LabelStud.io Blog",
url: "https://labelstud.io/blog/",
},
{
title: "Slack Community",
url: "https://slack.labelstud.io",
},
];

const actions = [
{
title: "Create Project",
icon: IconFolderAdd,
type: "createProject",
},
{
title: "Invite People",
icon: IconUserAdd,
type: "invitePeople",
},
] as const;

type Action = (typeof actions)[number]["type"];

export const HomePage: Page = () => {
const api = useAPI();
const [creationDialogOpen, setCreationDialogOpen] = useState(false);
const [invitationOpen, setInvitationOpen] = useState(false);
const { data, isFetching, isSuccess, isError } = useQuery({
queryKey: ["projects", { page_size: 10 }],
async queryFn() {
return api.callApi<{ results: APIProject[]; count: number }>("projects", {
params: { page_size: PROJECTS_TO_SHOW },
});
},
});

const handleActions = (action: Action) => {
return () => {
switch (action) {
case "createProject":
setCreationDialogOpen(true);
break;
case "invitePeople":
setInvitationOpen(true);
break;
}
};
};

return (
<main className="p-6">
<div className="grid grid-cols-[minmax(0,1fr)_450px] gap-6">
<section className="flex flex-col gap-6">
<div className="flex flex-col gap-1">
<Heading size={1}>Welcome 👋</Heading>
<Sub>Let's get you started.</Sub>
</div>
<div className="flex justify-between 2xl:justify-start gap-4">
{actions.map((action) => {
return (
<Button
key={action.title}
className="flex-1 2xl:flex-grow-0 text-lsLabelMedium text-lsPrimaryContent [&_svg]:w-6 [&_svg]:h-6"
variant="lsOutline"
onClick={handleActions(action.type)}
>
<action.icon className="text-lsPrimaryIcon" />
{action.title}
</Button>
);
})}
</div>

<SimpleCard
title={
data && data?.count > 0 ? (
<>
Recent Projects{" "}
<a href="/projects" className="text-lg font-normal hover:underline">
View All
</a>
</>
) : null
}
>
{isFetching ? (
<div className="h-64 flex justify-center items-center">
<Spinner />
</div>
) : isError ? (
<div className="h-64 flex justify-center items-center">can't load projects</div>
) : isSuccess && data.results.length === 0 ? (
<div className="flex flex-col justify-center items-center border border-lsBorderSubtle bg-lsPrimaryEmphasisSubtle rounded-lg h-64">
<div
className={
"rounded-full w-12 h-12 flex justify-center items-center bg-lsAccentGrapeSubtle text-lsPrimaryIcon"
}
>
<IconFolder />
</div>
<Heading size={2}>Create your first project</Heading>
<Sub>Import your data and set up the labeling interface to start annotating</Sub>
<Button className="mt-4" onClick={() => setCreationDialogOpen(true)}>
Create Project
</Button>
</div>
) : isSuccess && data.results.length > 0 ? (
<div className="flex flex-col gap-1">
{data.results.map((project) => {
return <ProjectSimpleCard key={project.id} project={project} />;
})}
</div>
) : null}
</SimpleCard>
</section>
<section className="flex flex-col gap-6">
<HeidiTips collection="projectSettings" />
<SimpleCard title="Resources" description="Learn, explore and get help">
<ul>
{resources.map((link) => {
return (
<li key={link.title}>
<a
href={link.url}
className="py-2 px-1 flex justify-between items-center text-lsNeutralContent"
target="_blank"
rel="noreferrer"
>
{link.title}
<IconExternal className="text-lsPrimaryIcon" />
</a>
</li>
);
})}
</ul>
</SimpleCard>
<div className="flex gap-2 items-center">
<IconHumanSignal />
<span className="text-lsNeutralContentSubtle">Label Studio Version: Community</span>
</div>
</section>
</div>
{creationDialogOpen && <CreateProject redirect={false} onClose={() => setCreationDialogOpen(false)} />}
<InviteLink opened={invitationOpen} onClosed={() => setInvitationOpen(false)} />
</main>
);
};

HomePage.title = "Home";
HomePage.path = "/";
HomePage.exact = true;

function ProjectSimpleCard({
project,
}: {
project: APIProject;
}) {
const finished = project.finished_task_number ?? 0;
const total = project.task_number ?? 0;
const progress = (total > 0 ? finished / total : 0) * 100;
const white = "#FFFFFF";
const color = project.color && project.color !== white ? project.color : "#E1DED5";

return (
<div className=" even:bg-lsNeutralSurface rounded-sm overflow-hidden">
<div
className="grid grid-cols-[minmax(0,1fr)_150px] p-2 py-3 items-center border-l-[3px]"
style={{ borderLeftColor: color }}
>
<div className="flex flex-col gap-1">
<a href={`/projects/${project.id}`} className="text-lsNeutralContent">
{project.title}
</a>
<div className="text-lsNeutralContentSubtler">
{finished} of {total} Tasks ({total > 0 ? Math.round((finished / total) * 100) : 0}%)
</div>
</div>
<div className="bg-lsNeutralSurface rounded-full overflow-hidden w-full h-2 shadow-lsNeutralBorderSubtle shadow-border-1">
<div className="bg-lsPositiveSurfaceHover h-full" style={{ maxWidth: `${progress}%` }} />
</div>
</div>
</div>
);
}
Loading

0 comments on commit e95280e

Please sign in to comment.