mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 09:26:47 +00:00
Added styling
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -25,7 +25,7 @@ export const EmptyState = ({
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={icon} size={iconSize} className="mr-4" />
|
<FontAwesomeIcon icon={icon} size={iconSize} />
|
||||||
<div className="flex flex-row items-center py-4">
|
<div className="flex flex-row items-center py-4">
|
||||||
<div className="text-sm text-bunker-300">{title}</div>
|
<div className="text-sm text-bunker-300">{title}</div>
|
||||||
<div>{children}</div>
|
<div>{children}</div>
|
||||||
|
|||||||
@@ -408,24 +408,12 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
<a>
|
<a>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
isSelected={router.asPath === `/project/${currentWorkspace?._id}/audit-logs`}
|
isSelected={router.asPath === `/project/${currentWorkspace?._id}/audit-logs`}
|
||||||
// icon={<FontAwesomeIcon icon={faFileLines} size="lg" />}
|
|
||||||
icon="system-outline-168-view-headline"
|
icon="system-outline-168-view-headline"
|
||||||
>
|
>
|
||||||
Audit Logs
|
Audit Logs
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={`/project/${currentWorkspace?._id}/secret-scanning`} passHref>
|
|
||||||
<a>
|
|
||||||
<MenuItem
|
|
||||||
isSelected={router.asPath === `/project/${currentWorkspace?._id}/secret-scanning`}
|
|
||||||
// icon={<FontAwesomeIcon icon={faFileLines} size="lg" />}
|
|
||||||
icon="system-outline-82-extension"
|
|
||||||
>
|
|
||||||
Audit Logs
|
|
||||||
</MenuItem>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
<Link href={`/project/${currentWorkspace?._id}/settings`} passHref>
|
<Link href={`/project/${currentWorkspace?._id}/settings`} passHref>
|
||||||
<a>
|
<a>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@@ -473,6 +461,16 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link href={`/org/${currentOrg?._id}/secret-scanning`} passHref>
|
||||||
|
<a>
|
||||||
|
<MenuItem
|
||||||
|
isSelected={router.asPath === `/org/${currentOrg?._id}/secret-scanning`}
|
||||||
|
icon="system-outline-69-document-scan"
|
||||||
|
>
|
||||||
|
Secret Scanning
|
||||||
|
</MenuItem>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
<Link href={`/org/${currentOrg?._id}/billing`} passHref>
|
<Link href={`/org/${currentOrg?._id}/billing`} passHref>
|
||||||
<a>
|
<a>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default function SettingsBilling() {
|
|||||||
<Head>
|
<Head>
|
||||||
<title>{t("common.head-title", { title: t("billing.title") })}</title>
|
<title>{t("common.head-title", { title: t("billing.title") })}</title>
|
||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel="icon" href="/infisical.ico" />
|
||||||
|
<meta property="og:image" content="/images/message.png" />
|
||||||
</Head>
|
</Head>
|
||||||
<BillingSettingsPage />
|
<BillingSettingsPage />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Head from "next/head";
|
||||||
|
import { useRouter } from "next/router"
|
||||||
|
|
||||||
|
import { Button } from "@app/components/v2";
|
||||||
|
import { SecretScanningLogsTable } from "@app/views/SecretScanning/components";
|
||||||
|
|
||||||
|
import createNewIntegrationSession from "../../../api/secret-scanning/createSecretScanningSession";
|
||||||
|
import getInstallationStatus from "../../../api/secret-scanning/getInstallationStatus";
|
||||||
|
import linkGitAppInstallationWithOrganization from "../../../api/secret-scanning/linkGitAppInstallationWithOrganization";
|
||||||
|
|
||||||
|
export default function SecretScanning() {
|
||||||
|
const router = useRouter()
|
||||||
|
const queryParams = router.query
|
||||||
|
const [integrationEnabled, setIntegrationStatus] = useState(false)
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
const linkInstallation = async () => {
|
||||||
|
if (typeof queryParams.state === "string" && typeof queryParams.installation_id === "string"){
|
||||||
|
try {
|
||||||
|
await linkGitAppInstallationWithOrganization(queryParams.installation_id as string, queryParams.state as string)
|
||||||
|
console.log("installation verification complete")
|
||||||
|
}catch (e){
|
||||||
|
console.log("app installation is stale, start new session", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchInstallationStatus = async () => {
|
||||||
|
const status = await getInstallationStatus(String(localStorage.getItem("orgData.id")))
|
||||||
|
setIntegrationStatus(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchInstallationStatus()
|
||||||
|
linkInstallation()
|
||||||
|
},[queryParams.state, queryParams.installation_id])
|
||||||
|
|
||||||
|
const generateNewIntegrationSession = async () => {
|
||||||
|
const session = await createNewIntegrationSession(String(localStorage.getItem("orgData.id")))
|
||||||
|
router.push(`https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Head>
|
||||||
|
<title>Secret scanning</title>
|
||||||
|
<link rel="icon" href="/infisical.ico" />
|
||||||
|
<meta property="og:image" content="/images/message.png" />
|
||||||
|
</Head>
|
||||||
|
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
|
||||||
|
<div className="max-w-7xl px-6 w-full">
|
||||||
|
<div className="mt-6 text-3xl font-semibold text-gray-200">Secret Scanning</div>
|
||||||
|
<div className="mb-6 text-lg text-mineshaft-300">Automatically monitor your GitHub activity and prevent secret leaks</div>
|
||||||
|
<div className="relative flex justify-between bg-mineshaft-800 border border-mineshaft-600 rounded-md p-6 mb-6">
|
||||||
|
<div className="flex flex-col items-start">
|
||||||
|
<div className="flex flex-row mb-1">Secret Scanning Status: {integrationEnabled ? <p className="text-green ml-1.5 font-semibold">Enabled</p> : <p className="text-red ml-1.5 font-semibold">Not enabled</p>}</div>
|
||||||
|
<div>{integrationEnabled ? <p className="text-mineshaft-300">Your GitHub organization is connected to Infisical, and is being continuously monitored for secret leaks.</p> : <p className="text-mineshaft-300">Connect your GitHub organization to Infisical.</p>}</div>
|
||||||
|
</div>
|
||||||
|
{integrationEnabled ? (
|
||||||
|
<div>
|
||||||
|
<div className="absolute right-[2.5rem] top-[2.5rem] animate-ping rounded-full h-6 w-6 bg-green flex items-center justify-center"/>
|
||||||
|
<div className="absolute right-[2.63rem] top-[2.63rem] animate-ping rounded-full h-5 w-5 bg-green flex items-center justify-center"/>
|
||||||
|
<div className="absolute right-[2.82rem] top-[2.82rem] animate-ping rounded-full h-3.5 w-3.5 bg-green flex items-center justify-center"/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center h-[3.25rem]">
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
colorSchema="primary"
|
||||||
|
onClick={generateNewIntegrationSession}
|
||||||
|
className="py-2 h-min"
|
||||||
|
>
|
||||||
|
Integrate with GitHub
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<SecretScanningLogsTable />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SecretScanning.requireAuth = true;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useRouter } from "next/router"
|
||||||
|
|
||||||
|
export default function SecretScanning() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
router.push(`${router.asPath.split("secret-scanning")[0]}/org/${localStorage.getItem("orgData.id")}/secret-scanning${router.asPath.split("secret-scanning")[1]}`)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
|
return <div/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
SecretScanning.requireAuth = true;
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import Head from "next/head";
|
|
||||||
import { useRouter } from "next/router"
|
|
||||||
|
|
||||||
import createNewIntegrationSession from "../api/secret-scanning/createSecretScanningSession";
|
|
||||||
import getInstallationStatus from "../api/secret-scanning/getInstallationStatus";
|
|
||||||
import getRisksByOrganization, { GitRisks } from "../api/secret-scanning/getRisksByOrganization";
|
|
||||||
import linkGitAppInstallationWithOrganization from "../api/secret-scanning/linkGitAppInstallationWithOrganization";
|
|
||||||
import { RiskStatus } from "../api/secret-scanning/updateRiskStatus";
|
|
||||||
|
|
||||||
export default function SecretScanning() {
|
|
||||||
const router = useRouter()
|
|
||||||
const queryParams = router.query
|
|
||||||
const [integrationEnabled, setIntegrationStatus] = useState(false)
|
|
||||||
const [gitRisks, setGitRisks] = useState<GitRisks[]>([]);
|
|
||||||
const [selectedRiskStatus, setSelectedRiskStatus] = useState("");
|
|
||||||
|
|
||||||
const handleSelectRiskStatusUpdate = (event: any) => {
|
|
||||||
setSelectedRiskStatus(event.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("selectedRiskStatus===>", selectedRiskStatus)
|
|
||||||
|
|
||||||
useEffect(()=>{
|
|
||||||
const fetchRisks = async () =>{
|
|
||||||
const risks = await getRisksByOrganization(String(localStorage.getItem("orgData.id")))
|
|
||||||
setGitRisks(risks)
|
|
||||||
}
|
|
||||||
|
|
||||||
const linkInstallation = async () => {
|
|
||||||
if (typeof queryParams.state === "string" && typeof queryParams.installation_id === "string"){
|
|
||||||
try {
|
|
||||||
await linkGitAppInstallationWithOrganization(queryParams.installation_id as string, queryParams.state as string)
|
|
||||||
console.log("installation verification complete")
|
|
||||||
}catch (e){
|
|
||||||
console.log("app installation is stale, start new session", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchInstallationStatus = async () => {
|
|
||||||
const status = await getInstallationStatus(String(localStorage.getItem("orgData.id")))
|
|
||||||
setIntegrationStatus(status)
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchInstallationStatus()
|
|
||||||
linkInstallation()
|
|
||||||
fetchRisks()
|
|
||||||
},[queryParams.state, queryParams.installation_id])
|
|
||||||
|
|
||||||
const generateNewIntegrationSession = async () => {
|
|
||||||
const session = await createNewIntegrationSession(String(localStorage.getItem("orgData.id")))
|
|
||||||
router.push(`https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Head>
|
|
||||||
<title>Secret scanning</title>
|
|
||||||
<link rel="icon" href="/infisical.ico" />
|
|
||||||
<meta property="og:image" content="/images/message.png" />
|
|
||||||
</Head>
|
|
||||||
{/* <NavHeader pageName={"Secret scanning"} isProjectRelated={false} /> */}
|
|
||||||
|
|
||||||
<div className="text-left">
|
|
||||||
{integrationEnabled ? (
|
|
||||||
<b className="text-green-500">Git app is linked to this organization</b>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
|
||||||
onClick={generateNewIntegrationSession}
|
|
||||||
>
|
|
||||||
Integrate with GitHub
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Date
|
|
||||||
</th>
|
|
||||||
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Secret Type
|
|
||||||
</th>
|
|
||||||
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
View Risk
|
|
||||||
</th>
|
|
||||||
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Info
|
|
||||||
</th>
|
|
||||||
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Status
|
|
||||||
</th>
|
|
||||||
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Action
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
|
||||||
{gitRisks.map((risk) => {
|
|
||||||
return (
|
|
||||||
<tr key={risk.ruleID}>
|
|
||||||
<td className="py-4 px-6 whitespace-nowrap">{risk.createdAt}</td>
|
|
||||||
<td className="py-4 px-6 whitespace-nowrap">{risk.ruleID}</td>
|
|
||||||
<td className="py-4 px-6 whitespace-nowrap">
|
|
||||||
<a
|
|
||||||
href={`https://github.com/${risk.repositoryFullName}/blob/${risk.commit}/${risk.file}#L${risk.startLine}-L${risk.endLine}`}
|
|
||||||
target="_blank"
|
|
||||||
className="text-red-500" rel="noreferrer"
|
|
||||||
>
|
|
||||||
View Exposed Secret
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td className="py-4 px-6 whitespace-nowrap">
|
|
||||||
<div className="font-bold">
|
|
||||||
<a href={`https://github.com/${risk.repositoryFullName}`}>
|
|
||||||
{risk.repositoryFullName}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs">
|
|
||||||
<span>{risk.file}</span><br/>
|
|
||||||
<br/>
|
|
||||||
<span className="font-bold">{risk.author}</span><br/>
|
|
||||||
<span>{risk.email}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="py-4 px-6 whitespace-nowrap">
|
|
||||||
{risk.isResolved ? "Resolved" : "Needs Attention"}
|
|
||||||
</td>
|
|
||||||
<td className="py-4 px-6 whitespace-nowrap">
|
|
||||||
{risk.isResolved ? "Resolved" : "Needs Attention"}
|
|
||||||
</td>
|
|
||||||
<td className="py-4 px-6 whitespace-nowrap">
|
|
||||||
<select
|
|
||||||
value={selectedRiskStatus}
|
|
||||||
onChange={handleSelectRiskStatusUpdate}
|
|
||||||
className="block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
>
|
|
||||||
<option>Unresolved</option>
|
|
||||||
<option value={RiskStatus.RESOLVED_FALSE_POSITIVE}>This is a false positive</option>
|
|
||||||
<option value={RiskStatus.RESOLVED_REVOKED}>I have rotated the secret, resolve risk</option>
|
|
||||||
<option value={RiskStatus.RESOLVED_NOT_REVOKED}>No rotate needed, resolve</option>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
import {
|
||||||
|
EmptyState,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr} from "@app/components/v2";
|
||||||
|
import getRisksByOrganization, { GitRisks } from "@app/pages/api/secret-scanning/getRisksByOrganization";
|
||||||
|
import { RiskStatus } from "@app/pages/api/secret-scanning/updateRiskStatus";
|
||||||
|
|
||||||
|
export const SecretScanningLogsTable = () => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [gitRisks, setGitRisks] = useState<GitRisks[]>([]);
|
||||||
|
const [selectedRiskStatus, setSelectedRiskStatus] = useState("");
|
||||||
|
|
||||||
|
const handleSelectRiskStatusUpdate = (event: any) => {
|
||||||
|
setSelectedRiskStatus(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchRisks = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
const risks = await getRisksByOrganization(String(localStorage.getItem("orgData.id")))
|
||||||
|
setGitRisks(risks);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchRisks();
|
||||||
|
},[])
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContainer className="mt-8">
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th className="flex-1">Date</Th>
|
||||||
|
<Th className="flex-1">Secret Type</Th>
|
||||||
|
<Th className="flex-1">View Risk</Th>
|
||||||
|
<Th className="flex-1">Info</Th>
|
||||||
|
<Th className="flex-1">Status</Th>
|
||||||
|
<Th className="flex-1">Action</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{!isLoading && gitRisks && gitRisks?.map((risk) => {
|
||||||
|
return (
|
||||||
|
<Tr key={risk.ruleID} className="h-10">
|
||||||
|
<Td>{risk.createdAt}</Td>
|
||||||
|
<Td>{risk.ruleID}</Td>
|
||||||
|
<Td>
|
||||||
|
<a
|
||||||
|
href={`https://github.com/${risk.repositoryFullName}/blob/${risk.commit}/${risk.file}#L${risk.startLine}-L${risk.endLine}`}
|
||||||
|
target="_blank"
|
||||||
|
className="text-red-500" rel="noreferrer"
|
||||||
|
>
|
||||||
|
View Exposed Secret
|
||||||
|
</a>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="font-bold">
|
||||||
|
<a href={`https://github.com/${risk.repositoryFullName}`}>
|
||||||
|
{risk.repositoryFullName}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs">
|
||||||
|
<span>{risk.file}</span><br/>
|
||||||
|
<br/>
|
||||||
|
<span className="font-bold">{risk.author}</span><br/>
|
||||||
|
<span>{risk.email}</span>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td>{risk.isResolved ? "Resolved" : "Needs Attention"}</Td>
|
||||||
|
<Td>
|
||||||
|
<select
|
||||||
|
value={selectedRiskStatus}
|
||||||
|
onChange={handleSelectRiskStatusUpdate}
|
||||||
|
className="block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
>
|
||||||
|
<option>Unresolved</option>
|
||||||
|
<option value={RiskStatus.RESOLVED_FALSE_POSITIVE}>This is a false positive</option>
|
||||||
|
<option value={RiskStatus.RESOLVED_REVOKED}>I have rotated the secret, resolve risk</option>
|
||||||
|
<option value={RiskStatus.RESOLVED_NOT_REVOKED}>No rotate needed, resolve</option>
|
||||||
|
</select>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{isLoading && <TableSkeleton columns={7} key="gitRisks" />}
|
||||||
|
{!isLoading && gitRisks && gitRisks?.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={7}>
|
||||||
|
<EmptyState
|
||||||
|
title="No risks detected."
|
||||||
|
icon={faCheck}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SecretScanningLogsTable } from "./SecretScanningLogsTable";
|
||||||
@@ -7,8 +7,8 @@ import {
|
|||||||
export const BillingSettingsPage = () => {
|
export const BillingSettingsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center bg-bunker-800 text-white w-full h-full px-6">
|
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
|
||||||
<div className="max-w-7xl w-full">
|
<div className="max-w-7xl px-6 w-full">
|
||||||
<div className="my-6">
|
<div className="my-6">
|
||||||
<p className="text-3xl font-semibold text-gray-200">{t("billing.title")}</p>
|
<p className="text-3xl font-semibold text-gray-200">{t("billing.title")}</p>
|
||||||
<div />
|
<div />
|
||||||
|
|||||||
Reference in New Issue
Block a user