forked from cerc-io/snowballtools-base
Implement routes for project tabs (#63)
* Add routes to project tabs * remove react tabs and use material tailwind component instead * Refactor code to move project tab panels in pages directory * Remove unused function from database class * Refactor routes for project tabs
This commit is contained in:
parent
a58b9b255e
commit
559e0f8934
@ -83,24 +83,6 @@ export class Database {
|
|||||||
return userOrgs;
|
return userOrgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectsByOrganizationId (organizationId: string): Promise<Project[]> {
|
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
|
||||||
|
|
||||||
const projects = await projectRepository.find({
|
|
||||||
relations: {
|
|
||||||
organization: true,
|
|
||||||
owner: true
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
organization: {
|
|
||||||
id: organizationId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return projects;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getProjectById (projectId: string): Promise<Project | null> {
|
async getProjectById (projectId: string): Promise<Project | null> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
|
||||||
|
@ -28,7 +28,6 @@
|
|||||||
"react-oauth-popup": "^1.0.5",
|
"react-oauth-popup": "^1.0.5",
|
||||||
"react-router-dom": "^6.20.1",
|
"react-router-dom": "^6.20.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"react-tabs": "^6.0.2",
|
|
||||||
"react-timer-hook": "^3.0.7",
|
"react-timer-hook": "^3.0.7",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"usehooks-ts": "^2.10.0",
|
"usehooks-ts": "^2.10.0",
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
|
|
||||||
import { Project } from 'gql-client';
|
|
||||||
|
|
||||||
import OverviewTabPanel from './OverviewTabPanel';
|
|
||||||
import DeploymentsTabPanel from './DeploymentsTabPanel';
|
|
||||||
import SettingsTabPanel from './SettingsTabPanel';
|
|
||||||
|
|
||||||
interface ProjectTabsProps {
|
|
||||||
project: Project;
|
|
||||||
onUpdate: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Database = () => (
|
|
||||||
<div>
|
|
||||||
Content of database tab
|
|
||||||
<p className="block">
|
|
||||||
It is a long established fact that a reader will be distracted by the
|
|
||||||
readable content of a page when looking at its layout.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
const Integrations = () => (
|
|
||||||
<div>
|
|
||||||
Content of integrations tab
|
|
||||||
<p className="block">
|
|
||||||
There are many variations of passages of Lorem Ipsum available.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const ProjectTabs = ({ project, onUpdate }: ProjectTabsProps) => {
|
|
||||||
return (
|
|
||||||
<Tabs
|
|
||||||
selectedTabClassName={
|
|
||||||
'border-b-2 border-gray-900 text-gray-900 focus:outline-none'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TabList className="flex border-b border-gray-300 text-gray-600">
|
|
||||||
<Tab className={'p-2 cursor-pointer'}>Overview</Tab>
|
|
||||||
<Tab className={'p-2 cursor-pointer'}>Deployments</Tab>
|
|
||||||
<Tab className={'p-2 cursor-pointer'}>Database</Tab>
|
|
||||||
<Tab className={'p-2 cursor-pointer'}>Integrations</Tab>
|
|
||||||
<Tab className={'p-2 cursor-pointer'}>Settings</Tab>
|
|
||||||
</TabList>
|
|
||||||
<TabPanel>
|
|
||||||
<OverviewTabPanel project={project} />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel>
|
|
||||||
<DeploymentsTabPanel project={project} />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel>
|
|
||||||
<Database />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel>
|
|
||||||
<Integrations />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel>
|
|
||||||
<SettingsTabPanel project={project} onUpdate={onUpdate} />
|
|
||||||
</TabPanel>
|
|
||||||
</Tabs>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProjectTabs;
|
|
@ -1,17 +1,30 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import {
|
||||||
|
Link,
|
||||||
|
Outlet,
|
||||||
|
useLocation,
|
||||||
|
useNavigate,
|
||||||
|
useParams,
|
||||||
|
} from 'react-router-dom';
|
||||||
import { Project as ProjectType } from 'gql-client';
|
import { Project as ProjectType } from 'gql-client';
|
||||||
|
|
||||||
import { Button, Typography } from '@material-tailwind/react';
|
import {
|
||||||
|
Button,
|
||||||
|
Tab,
|
||||||
|
Tabs,
|
||||||
|
TabsBody,
|
||||||
|
TabsHeader,
|
||||||
|
Typography,
|
||||||
|
} from '@material-tailwind/react';
|
||||||
|
|
||||||
import HorizontalLine from '../../../components/HorizontalLine';
|
import HorizontalLine from '../../../components/HorizontalLine';
|
||||||
import ProjectTabs from '../../../components/projects/project/ProjectTabs';
|
|
||||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||||
|
|
||||||
const Id = () => {
|
const Id = () => {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const [project, setProject] = useState<ProjectType | null>(null);
|
const [project, setProject] = useState<ProjectType | null>(null);
|
||||||
|
|
||||||
@ -22,6 +35,15 @@ const Id = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const currentTab = useMemo(() => {
|
||||||
|
if (id) {
|
||||||
|
const [, tabPath] = location.pathname.split(id);
|
||||||
|
return tabPath;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}, [location, id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchProject(id);
|
fetchProject(id);
|
||||||
}, [id]);
|
}, [id]);
|
||||||
@ -54,7 +76,44 @@ const Id = () => {
|
|||||||
</div>
|
</div>
|
||||||
<HorizontalLine />
|
<HorizontalLine />
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<ProjectTabs project={project} onUpdate={onUpdate} />
|
<Tabs value={currentTab}>
|
||||||
|
<TabsHeader
|
||||||
|
className="rounded-none border-b border-blue-gray-50 bg-transparent p-0"
|
||||||
|
indicatorProps={{
|
||||||
|
className:
|
||||||
|
'bg-transparent border-b-2 border-gray-900 shadow-none rounded-none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link to="">
|
||||||
|
<Tab value="" className={'p-2 cursor-pointer'}>
|
||||||
|
Overview
|
||||||
|
</Tab>
|
||||||
|
</Link>
|
||||||
|
<Link to="deployments">
|
||||||
|
<Tab value="/deployments" className={'p-2 cursor-pointer'}>
|
||||||
|
Deployments
|
||||||
|
</Tab>
|
||||||
|
</Link>
|
||||||
|
<Link to="database">
|
||||||
|
<Tab value="/database" className={'p-2 cursor-pointer'}>
|
||||||
|
Database
|
||||||
|
</Tab>
|
||||||
|
</Link>
|
||||||
|
<Link to="integrations">
|
||||||
|
<Tab value="/integrations" className={'p-2 cursor-pointer'}>
|
||||||
|
Integrations
|
||||||
|
</Tab>
|
||||||
|
</Link>
|
||||||
|
<Link to="settings">
|
||||||
|
<Tab value="/settings" className={'p-2 cursor-pointer'}>
|
||||||
|
Settings
|
||||||
|
</Tab>
|
||||||
|
</Link>
|
||||||
|
</TabsHeader>
|
||||||
|
<TabsBody>
|
||||||
|
<Outlet context={{ project, onUpdate }} />
|
||||||
|
</TabsBody>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@ -1,25 +1,31 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Project, Domain } from 'gql-client';
|
import { Domain } from 'gql-client';
|
||||||
|
import { useOutletContext } from 'react-router-dom';
|
||||||
|
|
||||||
import { Button, Typography } from '@material-tailwind/react';
|
import { Button, Typography } from '@material-tailwind/react';
|
||||||
|
|
||||||
import DeploymentDetailsCard from './deployments/DeploymentDetailsCard';
|
import DeploymentDetailsCard from '../../../../components/projects/project/deployments/DeploymentDetailsCard';
|
||||||
import FilterForm, {
|
import FilterForm, {
|
||||||
FilterValue,
|
FilterValue,
|
||||||
StatusOptions,
|
StatusOptions,
|
||||||
} from './deployments/FilterForm';
|
} from '../../../../components/projects/project/deployments/FilterForm';
|
||||||
import { DeploymentDetails } from '../../../types/project';
|
import {
|
||||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
DeploymentDetails,
|
||||||
import { COMMIT_DETAILS } from '../../../constants';
|
OutletContextType,
|
||||||
|
} from '../../../../types/project';
|
||||||
|
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||||
|
import { COMMIT_DETAILS } from '../../../../constants';
|
||||||
|
|
||||||
const DEFAULT_FILTER_VALUE: FilterValue = {
|
const DEFAULT_FILTER_VALUE: FilterValue = {
|
||||||
searchedBranch: '',
|
searchedBranch: '',
|
||||||
status: StatusOptions.ALL_STATUS,
|
status: StatusOptions.ALL_STATUS,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeploymentsTabPanel = ({ project }: { project: Project }) => {
|
const DeploymentsTabPanel = () => {
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
|
|
||||||
|
const { project } = useOutletContext<OutletContextType>();
|
||||||
|
|
||||||
const [filterValue, setFilterValue] = useState(DEFAULT_FILTER_VALUE);
|
const [filterValue, setFilterValue] = useState(DEFAULT_FILTER_VALUE);
|
||||||
const [deployments, setDeployments] = useState<DeploymentDetails[]>([]);
|
const [deployments, setDeployments] = useState<DeploymentDetails[]>([]);
|
||||||
const [prodBranchDomains, setProdBranchDomains] = useState<Domain[]>([]);
|
const [prodBranchDomains, setProdBranchDomains] = useState<Domain[]>([]);
|
@ -1,29 +1,26 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Domain, DomainStatus, Project } from 'gql-client';
|
import { Domain, DomainStatus } from 'gql-client';
|
||||||
|
import { useOutletContext } from 'react-router-dom';
|
||||||
|
|
||||||
import { Typography, Button, Chip } from '@material-tailwind/react';
|
import { Typography, Button, Chip } from '@material-tailwind/react';
|
||||||
|
|
||||||
import ActivityCard from './ActivityCard';
|
import ActivityCard from '../../../../components/projects/project/ActivityCard';
|
||||||
import { relativeTimeMs } from '../../../utils/time';
|
import { relativeTimeMs } from '../../../../utils/time';
|
||||||
import { useOctokit } from '../../../context/OctokitContext';
|
import { useOctokit } from '../../../../context/OctokitContext';
|
||||||
import { GitCommitDetails } from '../../../types/project';
|
import { GitCommitDetails, OutletContextType } from '../../../../types/project';
|
||||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||||
|
|
||||||
const COMMITS_PER_PAGE = 4;
|
const COMMITS_PER_PAGE = 4;
|
||||||
|
|
||||||
interface OverviewProps {
|
const OverviewTabPanel = () => {
|
||||||
project: Project;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Check if any live domain is set for production branch
|
|
||||||
|
|
||||||
const OverviewTabPanel = ({ project }: OverviewProps) => {
|
|
||||||
const { octokit } = useOctokit();
|
const { octokit } = useOctokit();
|
||||||
const [activities, setActivities] = useState<GitCommitDetails[]>([]);
|
const [activities, setActivities] = useState<GitCommitDetails[]>([]);
|
||||||
const [liveDomain, setLiveDomain] = useState<Domain>();
|
const [liveDomain, setLiveDomain] = useState<Domain>();
|
||||||
|
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
|
|
||||||
|
const { project } = useOutletContext<OutletContextType>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!octokit) {
|
if (!octokit) {
|
||||||
return;
|
return;
|
@ -1,5 +1,5 @@
|
|||||||
import React, { createElement } from 'react';
|
import React, { createElement } from 'react';
|
||||||
import { Project } from 'gql-client';
|
import { useOutletContext } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Tabs,
|
Tabs,
|
||||||
@ -9,11 +9,12 @@ import {
|
|||||||
TabPanel,
|
TabPanel,
|
||||||
} from '@material-tailwind/react';
|
} from '@material-tailwind/react';
|
||||||
|
|
||||||
import Domains from './settings/Domains';
|
import Domains from '../../../../components/projects/project/settings/Domains';
|
||||||
import GeneralTabPanel from './settings/GeneralTabPanel';
|
import GeneralTabPanel from '../../../../components/projects/project/settings/GeneralTabPanel';
|
||||||
import { EnvironmentVariablesTabPanel } from './settings/EnvironmentVariablesTabPanel';
|
import { EnvironmentVariablesTabPanel } from '../../../../components/projects/project/settings/EnvironmentVariablesTabPanel';
|
||||||
import GitTabPanel from './settings/GitTabPanel';
|
import GitTabPanel from '../../../../components/projects/project/settings/GitTabPanel';
|
||||||
import MembersTabPanel from './settings/MembersTabPanel';
|
import MembersTabPanel from '../../../../components/projects/project/settings/MembersTabPanel';
|
||||||
|
import { OutletContextType } from '../../../../types/project';
|
||||||
|
|
||||||
const tabsData = [
|
const tabsData = [
|
||||||
{
|
{
|
||||||
@ -48,13 +49,9 @@ const tabsData = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const SettingsTabPanel = ({
|
const SettingsTabPanel = () => {
|
||||||
project,
|
const { project, onUpdate } = useOutletContext<OutletContextType>();
|
||||||
onUpdate,
|
|
||||||
}: {
|
|
||||||
project: Project;
|
|
||||||
onUpdate: () => Promise<void>;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tabs
|
<Tabs
|
47
packages/frontend/src/pages/org-slug/projects/id/routes.tsx
Normal file
47
packages/frontend/src/pages/org-slug/projects/id/routes.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import OverviewTabPanel from './OverviewTabPanel';
|
||||||
|
import DeploymentsTabPanel from './DeploymentsTabPanel';
|
||||||
|
import SettingsTabPanel from './SettingsTabPanel';
|
||||||
|
|
||||||
|
const Database = () => (
|
||||||
|
<div>
|
||||||
|
Content of database tab
|
||||||
|
<p className="block">
|
||||||
|
It is a long established fact that a reader will be distracted by the
|
||||||
|
readable content of a page when looking at its layout.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Integrations = () => (
|
||||||
|
<div>
|
||||||
|
Content of integrations tab
|
||||||
|
<p className="block">
|
||||||
|
There are many variations of passages of Lorem Ipsum available.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const projectTabRoutes = [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
element: <OverviewTabPanel />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'deployments',
|
||||||
|
element: <DeploymentsTabPanel />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'database',
|
||||||
|
element: <Database />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'integrations',
|
||||||
|
element: <Integrations />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'settings',
|
||||||
|
element: <SettingsTabPanel />,
|
||||||
|
},
|
||||||
|
];
|
@ -5,6 +5,7 @@ import Id from './Id';
|
|||||||
import AddDomain from './id/domain/add';
|
import AddDomain from './id/domain/add';
|
||||||
import { createProjectRoutes } from './create/routes';
|
import { createProjectRoutes } from './create/routes';
|
||||||
import { addDomainRoutes } from './id/domain/add/routes';
|
import { addDomainRoutes } from './id/domain/add/routes';
|
||||||
|
import { projectTabRoutes } from './id/routes';
|
||||||
|
|
||||||
export const projectsRoutesWithoutSearch = [
|
export const projectsRoutesWithoutSearch = [
|
||||||
{
|
{
|
||||||
@ -23,5 +24,6 @@ export const projectsRoutesWithSearch = [
|
|||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
element: <Id />,
|
element: <Id />,
|
||||||
|
children: projectTabRoutes,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -60,3 +60,8 @@ export interface Commit {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
branch: string;
|
branch: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type OutletContextType = {
|
||||||
|
project: Project;
|
||||||
|
onUpdate: () => Promise<void>;
|
||||||
|
};
|
||||||
|
15
yarn.lock
15
yarn.lock
@ -6139,11 +6139,6 @@ clone@^1.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
|
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
|
||||||
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
|
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
|
||||||
|
|
||||||
clsx@^2.0.0:
|
|
||||||
version "2.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b"
|
|
||||||
integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==
|
|
||||||
|
|
||||||
cmd-shim@6.0.1:
|
cmd-shim@6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-6.0.1.tgz#a65878080548e1dca760b3aea1e21ed05194da9d"
|
resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-6.0.1.tgz#a65878080548e1dca760b3aea1e21ed05194da9d"
|
||||||
@ -13281,7 +13276,7 @@ promzard@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
read "^2.0.0"
|
read "^2.0.0"
|
||||||
|
|
||||||
prop-types@15.8.1, prop-types@^15.5.0, prop-types@^15.7.2, prop-types@^15.8.1:
|
prop-types@15.8.1, prop-types@^15.7.2, prop-types@^15.8.1:
|
||||||
version "15.8.1"
|
version "15.8.1"
|
||||||
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
|
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
|
||||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||||
@ -13623,14 +13618,6 @@ react-syntax-highlighter@^15.5.0:
|
|||||||
prismjs "^1.27.0"
|
prismjs "^1.27.0"
|
||||||
refractor "^3.6.0"
|
refractor "^3.6.0"
|
||||||
|
|
||||||
react-tabs@^6.0.2:
|
|
||||||
version "6.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-tabs/-/react-tabs-6.0.2.tgz#bc1065c3828561fee285a8fd045f22e0fcdde1eb"
|
|
||||||
integrity sha512-aQXTKolnM28k3KguGDBSAbJvcowOQr23A+CUJdzJtOSDOtTwzEaJA+1U4KwhNL9+Obe+jFS7geuvA7ICQPXOnQ==
|
|
||||||
dependencies:
|
|
||||||
clsx "^2.0.0"
|
|
||||||
prop-types "^15.5.0"
|
|
||||||
|
|
||||||
react-timer-hook@^3.0.7:
|
react-timer-hook@^3.0.7:
|
||||||
version "3.0.7"
|
version "3.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/react-timer-hook/-/react-timer-hook-3.0.7.tgz#ac42c43d0034b873cbf97b44eb34ccb2b11fe5e0"
|
resolved "https://registry.yarnpkg.com/react-timer-hook/-/react-timer-hook-3.0.7.tgz#ac42c43d0034b873cbf97b44eb34ccb2b11fe5e0"
|
||||||
|
Loading…
Reference in New Issue
Block a user