Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e7f4934ca | ||
|
|
3f1b9e89c8 | ||
|
|
034563d910 | ||
|
|
27ed0948a1 | ||
|
|
f1d1c63c90 | ||
|
|
732780b68d | ||
|
|
92132ad08b | ||
|
|
6904c242af | ||
|
|
3363303afb | ||
|
|
96e06db272 | ||
|
|
65df5495eb | ||
|
|
fa274711e8 | ||
|
|
741381d9a7 | ||
|
|
ab9a7b21c6 | ||
|
|
f3392ab1d5 | ||
|
|
281ec0a37b | ||
|
|
2a173dea7d | ||
|
|
3a3e475068 | ||
|
|
6e6324fdd3 | ||
|
|
688b4d6653 | ||
|
|
834b28c48e | ||
|
|
a63679ab04 | ||
|
|
afc9b5e25d | ||
|
|
7a1256e28a | ||
|
|
25d9d950c3 | ||
|
|
2308efbff6 | ||
|
|
43bb3a5a28 | ||
|
|
aff17088ae | ||
|
|
b3d424bda4 | ||
|
|
231953956c | ||
|
|
76339e4b37 | ||
|
|
0cf82e9cf3 | ||
|
|
ed4765f8ec | ||
|
|
da1dbbf8b8 | ||
|
|
e73769c049 | ||
|
|
d60f803231 | ||
|
|
98f9b38c4d | ||
|
|
d0fce28f0a | ||
|
|
f17d945c75 | ||
|
|
5102250f54 | ||
|
|
2417ddecb3 | ||
|
|
ace9f06bad | ||
|
|
461c6bea77 | ||
|
|
578e4b9662 | ||
|
|
be8cd35448 | ||
|
|
7072a7c8e8 | ||
|
|
ead5bda91e | ||
|
|
ecca61b662 | ||
|
|
e8cf687002 | ||
|
|
16073e314a | ||
|
|
ded7af095c |
46
.github/osintbuddy.com/docs/architecture-guide.md
vendored
Executable file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: Architecture guide
|
||||
description: The overall design and architecture of OSINTBuddy.
|
||||
---
|
||||
|
||||
This document describes the high-level architecture of OSINTBuddy. Please be aware the project is in a very early beta state and major changes may occur!
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
On the highest level, OSINTBuddy is a thing which lets users build interactive node based project graphs on the client, which can be expanded through data transforms that pull from: custom plugin based sources, open data sources, search engines, or for whatever you decide to implement.
|
||||
|
||||
### Project Structure
|
||||
|
||||
```js
|
||||
frontend // react frontend
|
||||
backend
|
||||
├── backend
|
||||
│ ├── app
|
||||
│ │ ├── alembic // migrations
|
||||
│ │ ├── app
|
||||
│ │ │ ├── admin // sqladmin models
|
||||
│ │ │ ├── api // controllers/api endpoints
|
||||
│ │ │ │ └── api_v1
|
||||
│ │ │ ├── core // env settings/celery app/logger/hashing
|
||||
│ │ │ ├── crud // views/crud functions
|
||||
│ │ │ ├── db // db initialization and base model classes
|
||||
│ │ │ ├── models // relational data models
|
||||
│ │ │ ├── schemas // pydantic/validation schemas
|
||||
│ │ │ ├── plugins // data sources/user plugins
|
||||
│ │ │ └── tests
|
||||
│ │ └── scripts // startup/testing scripts used by docker
|
||||
├── microservice
|
||||
└ └── google-service // golang google crawlers
|
||||
```
|
||||
|
||||
|
||||
### Core Services
|
||||
- **Frontend - [React](https://legacy.reactjs.org/docs/getting-started.html)**
|
||||
- **Backend - [FastAPI](https://fastapi.tiangolo.com/)**
|
||||
- **Caching - [Redis](https://redis.io/docs/)**
|
||||
- **Graph database - [JanusGraph](https://janusgraph.org/) + [ScyllaDB](https://www.scylladb.com/)**
|
||||
- **Task queue - [Redis & Celery](https://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers/redis.html)**
|
||||
- **Relational database - [PostgreSQL](https://www.postgresql.org/docs/)**
|
||||
|
||||
7
.github/osintbuddy.com/docs/entities-api.md
vendored
Executable file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Entities API reference
|
||||
pageTitle: Entities API reference
|
||||
description:
|
||||
---
|
||||
|
||||
Coming soon. ETA 12-24 weeks from June 10th 2023.
|
||||
7
.github/osintbuddy.com/docs/entities.md
vendored
Executable file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Entities
|
||||
pageTitle: Entities
|
||||
description:
|
||||
---
|
||||
|
||||
Coming soon. ETA 12-24 weeks from June 10th 2023.
|
||||
41
.github/osintbuddy.com/docs/how-to-contribute.md
vendored
Executable file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: How to contribute
|
||||
description: Contributing to OSINTBuddy development.
|
||||
---
|
||||
We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:
|
||||
|
||||
- Reporting a bug
|
||||
- Discussing the current state of the code
|
||||
- Submitting a fix
|
||||
- Proposing new features
|
||||
- Becoming a maintainer
|
||||
|
||||
## We Develop with Github
|
||||
We use github to host code, to track issues and feature requests, as well as accept pull requests.
|
||||
|
||||
Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests:
|
||||
|
||||
1. Fork the repo and create your branch from `main`.
|
||||
2. If you've added code that should be tested, add tests.
|
||||
3. If you've changed APIs, update the documentation.
|
||||
4. Ensure the test suite passes.
|
||||
5. Make sure your code lints.
|
||||
6. Issue that pull request!
|
||||
|
||||
## Any contributions you make will be under the GNU AGPLv3 Software License
|
||||
In short, when you submit code changes, your submissions are understood to be under the same [GNU AGPLv3 License](https://choosealicense.com/licenses/agpl-3.0/) that covers the project. Feel free to contact the maintainers if that's a concern.
|
||||
|
||||
## Report bugs using Github's [issues](https://github.com/briandk/transcriptase-atom/issues)
|
||||
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](); it's that easy! **Great Bug Reports** tend to have:
|
||||
|
||||
- A quick summary and/or background
|
||||
- Steps to reproduce
|
||||
- Be specific!
|
||||
- What you expected would happen
|
||||
- What actually happens
|
||||
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
|
||||
|
||||
People *love* thorough bug reports. I'm not even kidding.
|
||||
|
||||
## License
|
||||
By contributing, you agree that your contributions will be licensed under its GNU AGPLv3 License.
|
||||
43
.github/osintbuddy.com/docs/intro/getting-started.md
vendored
Executable file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: Getting involved with OSINTBuddy
|
||||
pageTitle: "OSINTBuddy - Getting Started"
|
||||
description: "OSINTBuddy: The Open-Source Alternative to Maltego. Fetch data from different sources and returns the results as visual entities that you can explore step-by-step."
|
||||
---
|
||||
|
||||
Learn all the ways you can get involved with the OSINTBuddy community and the projects development. {% .lead %}
|
||||
|
||||
{% quick-links %}
|
||||
|
||||
|
||||
|
||||
|
||||
{% quick-link title="Plugin System" icon="plug" href="/docs/plugin-system" description="Learn how to build powerful plugins that can pull from any data source." /%}
|
||||
|
||||
{% quick-link title="OSINTBuddy Membership" icon="coin" href="https://opencollective.com/openinfolabs/projects/osintbuddy#contribute" description="Join the OSINTBuddy Society for unique benefits. This helps us stay free for everyone" /%}
|
||||
|
||||
|
||||
|
||||
{% /quick-links %}
|
||||
|
||||
|
||||
## Joining the community
|
||||
|
||||
[Join us on Discord](https://discord.gg/gsbbYHA3K3) where you can connect with fellow OSINT enthusiasts, exchange ideas, get help, and discuss the OSINTBuddy application. Our Discord server offers dedicated channels for various topics, including general discussions, bug reporting, and feature requests. People are encouraged to ask questions, share their experiences, and or contribute to the development of the application.
|
||||
|
||||
By joining the OSINTBuddy community, you'll not only gain access to insider dev logs and support but also have the opportunity to contribute to the development and growth of this platform.
|
||||
|
||||
## Bug reporting
|
||||
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/jerlendds/osintbuddy/issues/new?assignees=jerlendds&labels=Type%3A+Bug&projects=&template=bug_report.md&title=%5BBUG%5D+); it's that easy! **Great Bug Reports** tend to have:
|
||||
|
||||
- Error logs wrapped in code blocks
|
||||
- A quick summary and/or background
|
||||
- Steps to reproduce
|
||||
- What you expected would happen
|
||||
- What actually happens
|
||||
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
|
||||
|
||||
People *love* thorough bug reports! I'm not even kidding.
|
||||
|
||||
## Reporting Security Issues
|
||||
Please email the core contributors directly at [theosintbuddyproject@openinfolabs.com](mailto:theosintbuddyproject@openinfolabs.com) directly if you encounter a serious security issue (e.g. in a package we use, etc)
|
||||
|
||||
55
.github/osintbuddy.com/docs/intro/installation.md
vendored
Executable file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: Installation
|
||||
description: Fetch data from different sources and returns the results as visual entities that you can explore step-by-step
|
||||
---
|
||||
|
||||
Learn how to get OSINTBuddy set up in about 30 minutes. To start using the alpha version of OSINTBuddy, ensure you have the prerequisites then follow the installation steps.
|
||||
|
||||
|
||||
**Prerequisites:**
|
||||
- Git
|
||||
- Docker & Docker Compose
|
||||
- _Windows only_:
|
||||
- WSL2
|
||||
- Tabby.sh _(optional)_
|
||||
|
||||
*Note that if you're on **Windows** and want this project to work you need unix line endings [(context)](https://stackoverflow.com/a/13154031). Before cloning, run: `git config --global core.autocrlf false`*
|
||||
|
||||
|
||||
*Note that if you're running on an **Apple** device you will need to open your Docker app, select the **features in development** tab on the left hand side of the docker app, and enable/checkmark the `Use Rosetta for x86/64 emulation on Apple Silicon` option if you want this project to work*
|
||||
|
||||
|
||||
## OSINTBuddy Install Instructions
|
||||
1. Clone the repo and submodules
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/jerlendds/osintbuddy.git
|
||||
cd osintbuddy
|
||||
# using ssh?
|
||||
# git clone --recurse-submodules git@github.com:jerlendds/osintbuddy.git
|
||||
```
|
||||
|
||||
2. Install Docker & Compose
|
||||
- [Install Guide for Mac](https://docs.docker.com/desktop/install/mac-install/)
|
||||
- [Install Guide for Windows](https://docs.docker.com/desktop/install/windows-install/)
|
||||
- [Install Guide for Linux](https://docs.docker.com/desktop/install/linux-install/)
|
||||
|
||||
3. Start the stack with Docker:
|
||||
```bash
|
||||
# ./launcher # display usage information.
|
||||
# ./launcher bootstrap # first time developing osintbuddy? read .github/CONTRIBUTING.md then run
|
||||
# bootstrap to setup the development environments for the stack.
|
||||
./launcher start # start the osintbuddy app.
|
||||
# ./launcher stop # stop the osintbuddy app.
|
||||
```
|
||||
- **Note:** the stack will take a few minutes to startup while Solr and ScyllaDB configure themselves for JanusGraph. If you try to connect before all the databases are ready you will encounter errors.
|
||||
|
||||
|
||||
You can now access OSINTBuddy through the URLs provided for the frontend, backend, and documentation.
|
||||
- **Default login**:
|
||||
- username: `osintbuddy`
|
||||
- password: `osintbuddy`
|
||||
- **URLs**
|
||||
- Frontend: [localhost:3000](http://localhost:3000)
|
||||
- Casdoor: [localhost:45910](http://localhost:45910)
|
||||
- Backend: [localhost:48997/api](http://localhost:48997/api)
|
||||
- Documentation: [localhost:48997/docs](http://localhost:48997/docs)
|
||||
16
.github/osintbuddy.com/docs/intro/roadmap.md
vendored
Executable file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
title: "OSINTBuddy: The Roadmap"
|
||||
pageTitle: "OSINTBuddy - Feature Roadmap"
|
||||
description: "Learn more about the long term plans for the OSINTBuddy project."
|
||||
---
|
||||
|
||||
|
||||
{% timelines %}
|
||||
|
||||
{% timeline title="Prototype Graph and Entity Editor (v. Alpha)" description="Edit your entities sources directly from the OSINTBuddy Web UI then visit the Graph to transform and interact with your entities" releaseDate="Released on August 12th, 2023" /%}
|
||||
|
||||
{% timeline title="UI/UX Improvements" downloadLink="https://github.com/jerlendds/osintbuddy/archive/refs/heads/main.zip" description="Download the OSINTBuddy source code (AGPLv3 license) from the latest stable changes on Github." releaseDate="Released Recently" /%}
|
||||
|
||||
{% timeline title="Rest of the System" description="Join the Discord or become a member for the latest news on the OSINTBuddy project" releaseDate="Unreleased" /%}
|
||||
|
||||
{% /timelines %}
|
||||
7
.github/osintbuddy.com/docs/plugin-recipes.md
vendored
Executable file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Plugin recipes
|
||||
pageTitle: Plugin recipes
|
||||
description:
|
||||
---
|
||||
|
||||
Coming soon. ETA 12-24 weeks from June 10th 2023.
|
||||
88
.github/osintbuddy.com/docs/plugin-system.md
vendored
Executable file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: Plugin System
|
||||
description: Fetch data from different sources and returns the results as visual entities that you can explore step-by-step
|
||||
---
|
||||
|
||||
***Please note:** [OSINTBuddy plugins](https://github.com/jerlendds/osintbuddy-plugins) are still in early alpha and breaking changes may occasionally occur in the API. That said, if you remain on the `main` branch and avoid accessing protected methods we will try our best to avoid introducing breaking changes.*
|
||||
|
||||
The osintbuddy plugin system at its core is very simple. An `OBRegistry` class holds all registered `OBPlugin` classes within the application. This registry is loaded into the [osintbuddy application](https://github.com/jerlendds/osintbuddy/) where it is then used to load the available entities for the user when they access a project graph, load transforms when a user opens the context menu of a node, and perform transformations which expect a `Plugin.blueprint()` to be returned. The returned data of a transform decorated method will be automatically mapped to a [JanusGraph](https://janusgraph.org/) database through [gremlin](https://tinkerpop.apache.org/) according to the labels *(as snakecase)* you previously set in the classes `node` for whatever `Plugin.blueprint()`
|
||||
you return.
|
||||
|
||||
To make this a bit more clear please see the below example...
|
||||
|
||||
```py
|
||||
from pydantic import BaseModel
|
||||
import osintbuddy import transform, Plugin
|
||||
from osintbuddy.elements import TextInput, DropdownInput, Title, CopyText
|
||||
from osintbuddy.errors import OBPluginError
|
||||
|
||||
|
||||
class CSESearchResults(Plugin):
|
||||
label = "CSE Result"
|
||||
name = "CSE result"
|
||||
show_label = False # do not show this on the entities dialog
|
||||
# the user sees on the left of the project graph screen
|
||||
color = "#058F63"
|
||||
node = [
|
||||
Title(label="Result"),
|
||||
CopyText(label="URL"),
|
||||
CopyText(label="Cache URL"),
|
||||
]
|
||||
|
||||
|
||||
class CSESearchPlugin(Plugin):
|
||||
label = "CSE Search"
|
||||
name = "CSE search"
|
||||
color = "#2C7237"
|
||||
node = [
|
||||
[
|
||||
TextInput(label="Query", icon="search"),
|
||||
TextInput(label="Pages", icon="123", default="1"),
|
||||
],
|
||||
DropdownInput(label="CSE Categories", options=cse_link_options)
|
||||
]
|
||||
|
||||
@transform(label="To cse results", icon="search")
|
||||
async def transform_to_cse_results(
|
||||
self,
|
||||
node: BaseModel, # dynamically generated pydantic model
|
||||
# that is mapped from the above labels contained within `node`
|
||||
use # a pydantic model allowing you to access a selenium instance
|
||||
# (and eventually a gremlin graph and settings api)
|
||||
):
|
||||
results = []
|
||||
|
||||
if not node.query:
|
||||
raise OBPluginError((
|
||||
'You can send error messages to the user here'
|
||||
'if they forget to submit data or if some other error occurs'
|
||||
))
|
||||
|
||||
# notice how you can access data returned from the context menu
|
||||
# of this node; using the label name in snake case
|
||||
print(node.cse_categories, node.query, node.pages)
|
||||
|
||||
... # (removed code for clarity)
|
||||
|
||||
if resp:
|
||||
for result in resp["results"]:
|
||||
url = result.get("breadcrumbUrl", {})
|
||||
# some elements you can store more than just a string,
|
||||
# (these elements storing dicts are mapped
|
||||
# to janusgraph as properties with the names
|
||||
# result_title, result_subtitle, and result_text)
|
||||
blueprint = CSESearchResults.blueprint(
|
||||
result={
|
||||
"title": result.get("titleNoFormatting"),
|
||||
"subtitle": url.get("host") + url.get("crumbs"),
|
||||
"text": result.get("contentNoFormatting"),
|
||||
},
|
||||
url=result.get("unescapedUrl"),
|
||||
cache_url=result.get("cacheUrl"),
|
||||
)
|
||||
results.append(blueprint)
|
||||
# here we return a list of blueprints (blueprints are dicts)
|
||||
# but you can also return a single blueprint without a list
|
||||
return results
|
||||
|
||||
```
|
||||
7
.github/osintbuddy.com/docs/plugins-api.md
vendored
Executable file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Plugins API reference
|
||||
pageTitle: Plugins API reference
|
||||
description:
|
||||
---
|
||||
|
||||
Coming soon. ETA 12-24 weeks from June 10th 2023.
|
||||
12
.github/osintbuddy.com/docs/project-graphs.md
vendored
Executable file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: Project graphs
|
||||
pageTitle: Project graphs
|
||||
description:
|
||||
---
|
||||
|
||||
Project graphs allow you to visualize complicated data sets and the connections between your stored data.
|
||||
In addition, you can transform existing data into new insights through a customizable plugin system that's easy to use.
|
||||
|
||||

|
||||
|
||||
|
||||
8
.github/osintbuddy.com/docs/registry-api.md
vendored
Executable file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Registry API reference
|
||||
pageTitle: Registry API reference
|
||||
description:
|
||||
---
|
||||
## Share your plugins through the registry
|
||||
|
||||
Coming soon. ETA 12-18 weeks from July 10th 2023.
|
||||
7
.github/osintbuddy.com/docs/settings-api.md
vendored
Executable file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Settings API reference
|
||||
pageTitle: Settings API reference
|
||||
description:
|
||||
---
|
||||
|
||||
Coming soon. ETA 12-24 weeks from June 10th 2023.
|
||||
7
.github/osintbuddy.com/docs/transforms-api.md
vendored
Executable file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Transforms API reference
|
||||
pageTitle: Transforms API reference
|
||||
description:
|
||||
---
|
||||
|
||||
Coming soon. ETA 12-24 weeks from June 10th 2023.
|
||||
6
.github/osintbuddy.com/docs/transforms.md
vendored
Executable file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Transforms
|
||||
pageTitle: Transforms
|
||||
description:
|
||||
---
|
||||
|
||||
26
.github/osintbuddy.com/docs/understanding-osintbuddy.md
vendored
Executable file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: "Understanding OSINTBuddy"
|
||||
pageTitle: "OSINTBuddy - Overview"
|
||||
description: "Understand the OSINTBuddy platform, its features, and how it can help enhance your open-source intelligence investigations."
|
||||
---
|
||||
|
||||
## An overview
|
||||
|
||||
OSINTBuddy is an open-source platform designed to assist with intelligence investigations. It offers a range of tools and features that enable investigators to collect, analyze, and visualize data from various sources. Below we will explore the key components of OSINTBuddy, their benefits, and how it can help enhance your OSINT capabilities.
|
||||
|
||||
### Data Collection
|
||||
|
||||
OSINTBuddy supports a wide range of data sources, including network information, search engines, and custom APIs. Users can easily configure and connect to these sources, pulling relevant data into their investigations.
|
||||
|
||||
### Plugin System
|
||||
|
||||
The plugin system is a core feature of OSINTBuddy that allows users to create, modify, and share plugins for specific tasks. This provides a flexible and customizable approach to data gathering and analysis, ensuring that OSINTBuddy remains adaptable to the ever-changing landscape of open-source intelligence.
|
||||
|
||||
### Visualization
|
||||
|
||||
To aid in the understanding of complex data sets, OSINTBuddy offers an intuitive visualization system that enables users to create interactive graphs of information that can be explored and transformed into new types of data. This helps investigators to better comprehend the relationships between data points, entities, and networks, making it easier to identify leads or potential targets.
|
||||
|
||||
|
||||
### Analytics & Inference
|
||||
|
||||
A whitepaper focused on the OSINT domain will be here eventually...
|
||||
72
.github/osintbuddy.com/docs/writing-plugins.md
vendored
Executable file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: Writing plugins
|
||||
pageTitle: OSINTBuddy - Creating your first plugin
|
||||
description:
|
||||
---
|
||||
|
||||
## Creating your first plugin
|
||||
|
||||
In this guide, we will dive into creating a custom plugin for OSINTBuddy that extends its capabilities. This will enable you to incorporate additional sources and transformations into the OSINTBuddy toolbox. We will use the provided code example as a reference to build our plugin.
|
||||
|
||||
### Step-by-step guide to create a new plugin
|
||||
|
||||
Start by importing the necessary modules:
|
||||
|
||||
```py
|
||||
import socket
|
||||
import osintbuddy as ob
|
||||
```
|
||||
|
||||
Define a class for your plugin, inheriting from `ob.Plugin`. Set the required attributes such as `label`, and optionally set an `icon` like `world-www` (using [tabler-icon](https://tabler-icons.io/) names), and a `node` which contains a list of elements used as a blueprint for creating the node displayed on the OSINTBuddy UI.
|
||||
|
||||
```py
|
||||
class WebsitePlugin(ob.Plugin):
|
||||
label = 'Website'
|
||||
name = 'Website'
|
||||
color = '#1D1DB8'
|
||||
icon = 'world-www'
|
||||
node = [
|
||||
ob.elements.TextInput(label='Domain', icon='world-www'),
|
||||
]
|
||||
```
|
||||
|
||||
Now, define a transformation method to gather and transform the data from the input node. In this case, the transform_to_ip function uses the `socket.gethostbyname()` Python module to obtain the IP address for a given domain. Decorate the method with `@transform()` and set the metadata attributes such as `label` and `icon`.
|
||||
|
||||
```py
|
||||
@ob.transform(label='To IP', icon='building-broadcast-tower')
|
||||
def transform_to_ip(self, node, **kwargs):
|
||||
blueprint = IPAddressPlugin.blueprint(
|
||||
ip_address=socket.gethostbyname(node.domain)
|
||||
)
|
||||
return blueprint
|
||||
```
|
||||
## Using the new plugin
|
||||
|
||||
To use the new plugin, simply add it to the existing OSINTBuddy plugins folder inside the application, in development mode it will be detected automatically and loaded by the platform. Once added, you can access your 'Website' plugin and use the 'To IP' transformation.
|
||||
|
||||
Creating a custom plugin for OSINTBuddy is an easy and effective way to enhance the capabilities of the tool, allowing you to fetch information from additional sources or transform the data in new ways. By following this guide and using the provided code example as a reference, you'll be able to create your own unique plugins to extend the functionality of OSINTBuddy to fit your specific needs.
|
||||
|
||||
|
||||
### Full example
|
||||
|
||||
```py
|
||||
import socket
|
||||
import osintbuddy as ob
|
||||
|
||||
class WebsitePlugin(ob.Plugin):
|
||||
label = 'Website'
|
||||
name = 'Website'
|
||||
color = '#1D1DB8'
|
||||
icon = 'world-www'
|
||||
node = [
|
||||
ob.elements.TextInput(label='Domain', icon='world-www'),
|
||||
]
|
||||
|
||||
@ob.transform(label='To IP', icon='building-broadcast-tower')
|
||||
def transform_to_ip(self, node, use):
|
||||
blueprint = IPAddressPlugin.blueprint(
|
||||
ip_address=socket.gethostbyname(node.domain)
|
||||
)
|
||||
return blueprint
|
||||
```
|
||||
|
||||
37
.github/osintbuddy.com/index.md
vendored
Executable file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: "OSINTBuddy: Transformative Link Analysis"
|
||||
pageTitle: "OSINTBuddy: Transformative Link Analysis, a Maltego Alternative"
|
||||
description: "OSINTBuddy: Fetch data from different sources and return the results as visual entities that you can explore step-by-step. The Open-Source Link Analysis Solution."
|
||||
---
|
||||
|
||||
|
||||
|
||||
OSINTBuddy provides a range of tools for performing OSINT research. One of the key features of OSINTBuddy is its plugin system which allows researchers to easily add or modify Python plugins _(also referred to as "entities")_ to suit their specific needs, or they can choose from any community-developed entities. This system provides a flexible and customizable approach to data gathering and analysis, making it an open-source alternative to popular proprietary tools such as Maltego.
|
||||
|
||||
{% quick-links %}
|
||||
|
||||
{% quick-link title="Installation" icon="installation" href="/docs/installation" description="Step-by-step guides for setting up your system and installing OSINTBuddy." /%}
|
||||
[Title](index.md) [Title](docs/writing-plugins.md) [Title](docs/understanding-osintbuddy.md) [Title](docs/transforms-api.md) [Title](docs/transforms.md) [Title](docs/settings-api.md) [Title](docs/registry-api.md) [Title](docs/project-graphs.md) [Title](docs/plugins-api.md) [Title](docs/plugin-system.md) [Title](docs/plugin-recipes.md) [Title](docs/how-to-contribute.md) [Title](docs/entities.md) [Title](docs/entities-api.md) [Title](docs/architecture-guide.md) [Title](docs/intro/roadmap.md) [Title](docs/intro/getting-started.md) [Title](docs/intro) [Title](docs/intro/installation.md)
|
||||
{% quick-link title="Architecture overview" icon="presets" href="/docs/architecture-guide" description="Learn how the internals work and start contributing to OSINTBuddy." /%}
|
||||
|
||||
{% quick-link title="Plugin System" icon="plug" href="/docs/plugin-system" description="Learn how to build powerful plugins that can pull from any data source." /%}
|
||||
|
||||
{% quick-link title="OSINTBuddy Membership" icon="coin" href="https://opencollective.com/openinfolabs/projects/osintbuddy#contribute" description="Join the OSINTBuddy Society for unique benefits. This helps us stay free for everyone" /%}
|
||||
|
||||
|
||||
{% /quick-links %}
|
||||
|
||||
|
||||
## How the Plugin System Works
|
||||
|
||||
The plugin system works by allowing users to create and share small, focused modules that perform specific tasks, such as extracting information from a social media profile, analyzing email headers, or mapping IP addresses to geographical locations. These individual plugins can then be combined and connected in various ways to build more complex data-mining workflows.
|
||||
|
||||
|
||||
## The Benefits of an Open-Source Alternative
|
||||
|
||||
As an open-source project, OSINTBuddy offers several advantages over proprietary tools like [Maltego](https://www.maltego.com/). The most significant of these is the ability for users to actively participate in the development process, suggesting improvements, reporting bugs, and [contributing their own plugins and enhancements](/docs/registry-api).
|
||||
|
||||
This collaborative approach ensures that OSINTBuddy remains responsive to the needs of its users, continually evolving to address new challenges and opportunities in the rapidly changing world of open-source intelligence.
|
||||
|
||||
Moreover, as an open-source project, OSINTBuddy is freely available to all, lowering the barriers to entry for individuals and organizations seeking to leverage the power of OSINT in their work. This democratizes access to powerful intelligence-gathering tools and fosters a more diverse and inclusive community of practitioners.
|
||||
|
||||
45
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Our Code of Conduct
|
||||
|
||||
## Hacker Ethics and Our Pledge
|
||||
|
||||
In the interest of _fostering an respectful, libre, and open source environment,_ we as hackers,
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for anyone. Respect has great importance in everyday life. Calls to respect this or that are increasingly part of public life:
|
||||
|
||||
- environmentalists exhort us to respect nature.
|
||||
- foes of abortion and capital punishment insist on respect for human life.
|
||||
- members of racial and ethnic minorities and those discriminated against because of their gender, sexual orientation, age, religious beliefs.
|
||||
- economic status demand respect both as social and moral equals and for their cultural differences.
|
||||
|
||||
It is widely acknowledged that **public debates about such demands should take place under terms of mutual respect**. We may learn both that our lives together go better when we respect the things that deserve to be respected.[[1]](https://plato.stanford.edu/entries/respect/) Hackers can come in many forms, many undertakings might now be considered variants of hacking, from modding cars to crafting to do-it-yourself projects.
|
||||
|
||||
**We don’t care if you’re liberal or conservative, Black or white, straight or gay, or anything in between!** In fact, we won’t bring it up, or ask. **We simply do not care.** **This is a place to experiment with and then practice leaving behind categories of social difference while remaining respectful.** We all make mistakes, overstep ourselves at times, introduce bugs, bring down production, lie in our documentation, get frustrated, implement dark UI patterns, or any number of other mistakes, but when we get called out, we will make the choice to try and understand the others point of view rather than attacking or trying to ruin someones life. This is an open-technology community that aims to be a space of refuge for any participants, a space of sanctuary. From programmers, to translators, designers, writers and beyond, we will all seek to understand how and why things are the way they are before catalyzing or proposing a transition to a future state.
|
||||
|
||||
We believe that from ethics, enlightened self-interest, and the commonwealth, our governance will emerge. Our identities may be distributed across many of your jurisdictions. The only law that all our constituent cultures would generally recognize is the [Golden Rule](https://en.wikipedia.org/wiki/Golden_Rule).
|
||||
|
||||
## Our Standards - The ethical principles of hacking - motivation and limits
|
||||
|
||||
* Accept constructive criticism and be respectful of differing viewpoints and experiences while showing empathy towards other.
|
||||
* Access to computers - and anything which might teach you something about the way the world really works - should be unlimited and total. Always yield to the Hands-On Imperative!
|
||||
* All information should be free.
|
||||
* Mistrust authority - promote decentralization.
|
||||
* Hackers should be judged by their acting, not bogus criteria such as degrees, age, gender, race, or position.
|
||||
* You can create art and beauty on a computer.
|
||||
* Computers can change your life for the better.
|
||||
* Don't litter other people's data.
|
||||
* Make public data available, protect private data.
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of any mistakes in behavior.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
@@ -37,7 +37,7 @@
|
||||
🚧 <ins>Work in progress</ins> 🚧
|
||||
<br/>
|
||||
|
||||
[2023-12-12_19-07.webm](https://github.com/jerlendds/osintbuddy/assets/29207058/307ddc96-251d-4830-9fdc-c2a3719de369)
|
||||
[2024-01-09 20-55.webm](https://github.com/jerlendds/osintbuddy/assets/29207058/fb07fa95-30af-45aa-a75b-2fa1473bc37b)
|
||||
|
||||
|
||||
⚠️ **Searching for contributors**, please contact me on the **[forum (work-in-progress)](https://forum.osintbuddy.com)**
|
||||
@@ -233,8 +233,8 @@ Patched `aiogremlin` library: [jerlendds/gremlinpy](https://github.com/jerlendds
|
||||
|
||||
|
||||
## [↑](#introducing-osintbuddy)Sponsor OSINTBuddy
|
||||
Help us keep the project free and maintained.
|
||||
Sponsor the OSINTBuddy project for unique benefits. Learn more on the [OSINTBuddy OpenCollective](https://opencollective.com/openinfolabs/projects/osintbuddy#category-CONTRIBUTE)
|
||||
Help us keep the project free and maintained. Sponsoring the project makes it possible for us to cover our server costs and allows us to make investments into new areas of development.
|
||||
Depending on your level of sponsorship you may get unique benefits. Learn more on the [OSINTBuddy OpenCollective](https://opencollective.com/openinfolabs/projects/osintbuddy#category-CONTRIBUTE)
|
||||
|
||||
[](https://opencollective.com/openinfolabs/projects/osintbuddy#category-CONTRIBUTE)
|
||||
[](https://opencollective.com/openinfolabs/projects/osintbuddy#category-CONTRIBUTE)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from fastapi import APIRouter
|
||||
from app.core.config import settings
|
||||
|
||||
from .endpoints import entity, graph, node, login, scan, account, config
|
||||
from .endpoints import entity, graph, graph_inquiry, login, scan, account, config
|
||||
|
||||
api_router = APIRouter(prefix=settings.API_V1_STR)
|
||||
|
||||
api_router.include_router(node.router, tags=["Nodes"])
|
||||
api_router.include_router(graph_inquiry.router, tags=["Nodes"])
|
||||
api_router.include_router(config.router, tags=["Config"])
|
||||
api_router.include_router(login.router, tags=["Login"])
|
||||
api_router.include_router(account.router, tags=["Accounts"])
|
||||
|
||||
@@ -14,12 +14,12 @@ router = APIRouter(prefix="/account")
|
||||
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=Union[schemas.User, schemas.HTTPError],
|
||||
response_model=Union[schemas.UserInDBBase, schemas.HTTPError],
|
||||
response_model_exclude_none=True
|
||||
)
|
||||
async def get_account(
|
||||
request: APIRequest,
|
||||
user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
|
||||
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)],
|
||||
) -> models.User:
|
||||
try:
|
||||
return user
|
||||
|
||||
@@ -15,7 +15,7 @@ router = APIRouter(prefix="/config")
|
||||
@router.get("/casdoor", response_model=Any)
|
||||
async def get_casdoor_config(
|
||||
request: APIRequest,
|
||||
user: Annotated[schemas.User, Depends(deps.get_user_from_session)]
|
||||
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)]
|
||||
):
|
||||
sdk = request.app.state.CASDOOR_SDK
|
||||
casdoor_apps = sdk.get_applications()
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import os, importlib, inspect
|
||||
from uuid import UUID
|
||||
from typing import Annotated
|
||||
from app.schemas.entities import ENTITY_NAMESPACE
|
||||
|
||||
from osintbuddy import Registry
|
||||
from app.schemas.entities import ENTITY_NAMESPACE, Entity
|
||||
from app.api.utils import get_hid
|
||||
from osintbuddy.plugins import EntityRegistry, load_plugin_source
|
||||
from osintbuddy.templates.default import plugin_source_template
|
||||
from osintbuddy.utils.generic import to_snake_case
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
@@ -10,7 +12,6 @@ from sqlalchemy.orm import Session
|
||||
from starlette import status
|
||||
|
||||
from app.api import deps
|
||||
from app.api.api_v1.endpoints.node import refresh_local_entities
|
||||
from app import crud, schemas
|
||||
from app.core.logger import get_logger
|
||||
|
||||
@@ -19,15 +20,14 @@ router = APIRouter(prefix="/entity")
|
||||
|
||||
|
||||
async def fetch_node_transforms(plugin_label):
|
||||
plugin = await Registry.get_plugin(plugin_label=to_snake_case(plugin_label))
|
||||
plugin = await EntityRegistry.get_plugin(plugin_label=to_snake_case(plugin_label))
|
||||
if plugin is not None:
|
||||
labels = plugin().transform_labels
|
||||
return labels
|
||||
return plugin().transform_labels
|
||||
|
||||
|
||||
@router.get("/plugins/transform/")
|
||||
async def get_entity_transforms(
|
||||
user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
|
||||
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)],
|
||||
label: str
|
||||
):
|
||||
try:
|
||||
@@ -41,8 +41,8 @@ async def get_entity_transforms(
|
||||
detail="Invalid transform error. Please file an issue if this occurs."
|
||||
)
|
||||
except Exception as e:
|
||||
log.error("Error inside entity.get_entity_transforms")
|
||||
log.error(e)
|
||||
log.error("Error inside entity.get_entity_transforms")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="There was an error fetching transforms, please try again."
|
||||
@@ -51,13 +51,12 @@ async def get_entity_transforms(
|
||||
|
||||
@router.get("/details/{hid}", response_model=schemas.Entity)
|
||||
async def get_entity(
|
||||
user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
|
||||
hid: Annotated[str, Depends(deps.get_entity_id)],
|
||||
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)],
|
||||
hid: Annotated[int, Depends(deps.get_entity_id)],
|
||||
db: Annotated[Session, Depends(deps.get_db)],
|
||||
):
|
||||
try:
|
||||
entities = crud.entities.get(db=db, id=hid)
|
||||
return entities
|
||||
return crud.entities.get(db=db, id=hid)
|
||||
except Exception as e:
|
||||
log.error('Error inside entity.get_entity:')
|
||||
log.error(e)
|
||||
@@ -72,7 +71,7 @@ async def get_entity(
|
||||
response_model=schemas.AllEntitiesList
|
||||
)
|
||||
async def get_entities(
|
||||
user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
|
||||
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)],
|
||||
db: Annotated[Session, Depends(deps.get_db)],
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
@@ -80,6 +79,22 @@ async def get_entities(
|
||||
# try:
|
||||
if limit > 50:
|
||||
limit = 50
|
||||
|
||||
EntityRegistry.discover_plugins()
|
||||
for e in EntityRegistry.entities:
|
||||
entity_file = open(inspect.getsourcefile(e), "r")
|
||||
file_entity = schemas.EntityCreate(
|
||||
label=e.label,
|
||||
author=e.author,
|
||||
description=e.description,
|
||||
source=entity_file.read()
|
||||
)
|
||||
entity_obj = crud.entities.get_by_label(db=db, label=file_entity.label)
|
||||
if entity_obj:
|
||||
crud.entities.update(db, db_obj=entity_obj, obj_in=file_entity)
|
||||
else:
|
||||
crud.entities.create(db, obj_in=file_entity)
|
||||
|
||||
db_entities, entities_count = crud.entities.get_many_by_favorites(
|
||||
db=db,
|
||||
skip=skip,
|
||||
@@ -94,16 +109,17 @@ async def get_entities(
|
||||
)
|
||||
|
||||
entities = []
|
||||
favorite_entities = []
|
||||
|
||||
for entity in db_entities:
|
||||
entity = entity._asdict()
|
||||
entity["id"] = deps.hid(db_id=entity.get("id"), ns=ENTITY_NAMESPACE)
|
||||
entity["id"] = deps.get_hid(db_id=entity.get("id"), ns=ENTITY_NAMESPACE)
|
||||
entities.append(entity)
|
||||
favorite_entities = []
|
||||
for entity in db_favorite_entities:
|
||||
entity = entity._asdict()
|
||||
entity["id"] = deps.hid(db_id=entity.get("id"), ns=ENTITY_NAMESPACE)
|
||||
entity["id"] = deps.get_hid(db_id=entity.get("id"), ns=ENTITY_NAMESPACE)
|
||||
favorite_entities.append(entity)
|
||||
|
||||
|
||||
return {
|
||||
"entities": entities,
|
||||
"count": entities_count,
|
||||
@@ -120,12 +136,12 @@ async def get_entities(
|
||||
|
||||
@router.post("")
|
||||
async def create_entity(
|
||||
user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
|
||||
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)],
|
||||
entity: schemas.PostEntityCreate,
|
||||
db: Annotated[Session, Depends(deps.get_db)],
|
||||
):
|
||||
try:
|
||||
return crud.entities.create(db, obj_in=schemas.EntityCreate(
|
||||
obj_in = schemas.EntityCreate(
|
||||
label=entity.label,
|
||||
author=entity.author,
|
||||
description=entity.description,
|
||||
@@ -134,7 +150,8 @@ async def create_entity(
|
||||
description=entity.description,
|
||||
author=entity.author
|
||||
)
|
||||
))
|
||||
)
|
||||
return crud.entities.create(db, obj_in)
|
||||
except Exception as e:
|
||||
log.error('Error inside entity.create_entity:')
|
||||
log.error(e)
|
||||
@@ -146,19 +163,17 @@ async def create_entity(
|
||||
|
||||
@router.put("/{hid}")
|
||||
async def update_entity_by_id(
|
||||
user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
|
||||
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)],
|
||||
hid: Annotated[str, Depends(deps.get_entity_id)],
|
||||
obj_in: schemas.EntityBase,
|
||||
obj_in: schemas.EntityUpdate,
|
||||
db: Annotated[Session, Depends(deps.get_db)],
|
||||
):
|
||||
try:
|
||||
db_obj = crud.entities.get(db=db, id=hid)
|
||||
entity = crud.entities.update(db=db, db_obj=db_obj, obj_in=obj_in)
|
||||
refresh_local_entities(db)
|
||||
|
||||
return entity
|
||||
except Exception as e:
|
||||
log.error('Error inside entity.update_entity_by_uuid:')
|
||||
log.error('Error inside entity.update_entity_by_id:')
|
||||
log.error(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
@@ -168,7 +183,7 @@ async def update_entity_by_id(
|
||||
|
||||
@router.delete("/{hid}")
|
||||
async def delete_entity(
|
||||
user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
|
||||
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)],
|
||||
hid: Annotated[str, Depends(deps.get_entity_id)],
|
||||
db: Annotated[Session, Depends(deps.get_db)],
|
||||
):
|
||||
@@ -185,7 +200,7 @@ async def delete_entity(
|
||||
|
||||
@router.put("/{hid}/favorite")
|
||||
async def update_entity_favorite_id(
|
||||
user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
|
||||
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)],
|
||||
hid: Annotated[str, Depends(deps.get_entity_id)],
|
||||
db: Annotated[Session, Depends(deps.get_db)],
|
||||
):
|
||||
|
||||
@@ -8,7 +8,7 @@ from app.api import deps
|
||||
from app import crud, schemas
|
||||
from app.core.logger import get_logger
|
||||
from app.db.janus import ProjectGraphConnection, janus_create_db
|
||||
from app.api.utils import hid
|
||||
from app.api.utils import get_hid
|
||||
|
||||
log = get_logger("api_v1.endpoints.graph")
|
||||
router = APIRouter(prefix="/graph")
|
||||
@@ -16,7 +16,7 @@ router = APIRouter(prefix="/graph")
|
||||
|
||||
@router.get("/{hid}", response_model=schemas.Graph)
|
||||
async def get_graph(
|
||||
user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
|
||||
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)],
|
||||
hid: Annotated[str, Depends(deps.get_graph_id)],
|
||||
db: Annotated[Session, Depends(deps.get_db)],
|
||||
):
|
||||
@@ -37,7 +37,7 @@ async def get_graph(
|
||||
|
||||
@router.patch('/{hid}/favorite/')
|
||||
async def update_graph_favorite_id(
|
||||
user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
|
||||
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)],
|
||||
hid: Annotated[str, Depends(deps.get_graph_id)],
|
||||
db: Annotated[Session, Depends(deps.get_db)],
|
||||
):
|
||||
@@ -66,7 +66,7 @@ async def update_graph_favorite_id(
|
||||
response_model=schemas.AllGraphsList,
|
||||
)
|
||||
async def get_graphs(
|
||||
user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
|
||||
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)],
|
||||
db: Annotated[Session, Depends(deps.get_db)],
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
@@ -112,7 +112,7 @@ async def get_graphs(
|
||||
response_model=schemas.GraphsList
|
||||
)
|
||||
async def get_graphs_by_favorite(
|
||||
user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
|
||||
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)],
|
||||
db: Annotated[Session, Depends(deps.get_db)],
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
@@ -139,7 +139,7 @@ async def get_graphs_by_favorite(
|
||||
response_model=schemas.Graph
|
||||
)
|
||||
async def create_graph(
|
||||
user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
|
||||
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)],
|
||||
obj_in: schemas.GraphCreate,
|
||||
db: Annotated[Session, Depends(deps.get_db)],
|
||||
):
|
||||
@@ -162,7 +162,7 @@ async def create_graph(
|
||||
|
||||
@router.delete('')
|
||||
async def delete_graph(
|
||||
user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
|
||||
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)],
|
||||
hid: Annotated[str, Depends(deps.get_graph_id)],
|
||||
db: Annotated[Session, Depends(deps.get_db)],
|
||||
):
|
||||
@@ -180,7 +180,7 @@ async def delete_graph(
|
||||
|
||||
@router.get('/{hid}/stats')
|
||||
async def get_graph_stats(
|
||||
user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
|
||||
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)],
|
||||
hid: Annotated[str, Depends(deps.get_graph_id)],
|
||||
db: Annotated[Session, Depends(deps.get_db)],
|
||||
):
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
from uuid import UUID
|
||||
from typing import List, Callable, Tuple, Any, Annotated
|
||||
from typing import List, Callable, Tuple, Any, Annotated, Dict
|
||||
from fastapi import APIRouter, WebSocket, WebSocketException, WebSocketDisconnect, HTTPException, Depends
|
||||
from sqlalchemy import or_
|
||||
from websockets.exceptions import ConnectionClosedError
|
||||
from gremlinpy.process.graph_traversal import AsyncGraphTraversal
|
||||
from gremlin_python.process.graph_traversal import __ as _g
|
||||
from gremlin_python.process.traversal import P
|
||||
from gremlin_python.process.traversal import T, Cardinality
|
||||
from osintbuddy.utils import to_snake_case, MAP_KEY, chunks
|
||||
from osintbuddy import Registry, PluginUse, load_plugins
|
||||
from osintbuddy import EntityRegistry, TransformUse, EntityConfig
|
||||
from osintbuddy.errors import OBPluginError
|
||||
from sqlalchemy.orm import Session
|
||||
from starlette import status
|
||||
@@ -20,30 +22,19 @@ from app.core.config import settings
|
||||
log = get_logger("api_v1.endpoints.nodes")
|
||||
router = APIRouter(prefix="/node")
|
||||
|
||||
# TODO: Refactor to somewhere that makes sense...
|
||||
def refresh_local_entities(db: Session):
|
||||
Registry.plugins = []
|
||||
Registry.labels = []
|
||||
Registry.ui_labels = []
|
||||
entities = crud.entities.get_many(db, skip=0, limit=100)
|
||||
load_plugins(entities)
|
||||
|
||||
|
||||
async def fetch_node_transforms(plugin_label):
|
||||
plugin = await Registry.get_plugin(plugin_label=plugin_label)
|
||||
plugin = await EntityRegistry.get_plugin(plugin_label=plugin_label)
|
||||
if plugin is not None:
|
||||
labels = plugin().transform_labels
|
||||
return labels
|
||||
|
||||
|
||||
|
||||
def add_node_element(vertex, element: dict or List[dict], data_labels: List[str]):
|
||||
# Remove stylistic values unrelated to element data
|
||||
# Some osintbuddy.elements of type displays dont have an icon or options
|
||||
|
||||
icon = element.pop('icon', None)
|
||||
options = element.pop('options', None)
|
||||
|
||||
label = element.pop('label')
|
||||
elm_type = element.pop('type')
|
||||
|
||||
@@ -56,7 +47,6 @@ def add_node_element(vertex, element: dict or List[dict], data_labels: List[str]
|
||||
vertex.property(f'{to_snake_case(label)}_{to_snake_case(k)}', v)
|
||||
# Save the data labels so we can assign these as meta properties later
|
||||
data_labels.append(to_snake_case(label))
|
||||
|
||||
element['type'] = elm_type
|
||||
element['icon'] = icon
|
||||
element['label'] = label
|
||||
@@ -117,7 +107,11 @@ async def save_node_on_drop(
|
||||
}
|
||||
}
|
||||
|
||||
async def load_initial_graph(uuid: UUID) -> tuple[list, list]:
|
||||
async def load_nodes_from_db(uuid: UUID, viewport=None) -> tuple[list, list]:
|
||||
zoom_scale = viewport.get('zoom')
|
||||
# x = viewport.get('x')
|
||||
# y = viewport.get('y')
|
||||
|
||||
async with ProjectGraphConnection(uuid) as graph:
|
||||
edges: list = await graph.V().outE().project('from', 'edge', 'to')\
|
||||
.by(_g.outV()).by(_g.valueMap(True)).by(_g.inV()).union(
|
||||
@@ -125,8 +119,20 @@ async def load_initial_graph(uuid: UUID) -> tuple[list, list]:
|
||||
_g.project('from').by(_g.select('from')).unfold(),
|
||||
_g.project('to').by(_g.select('to')).unfold()
|
||||
).fold().toList()
|
||||
nodes: list = await graph.V().valueMap(True).toList()
|
||||
|
||||
# TODO: Load/update UI nodes on far drag
|
||||
# TODO: unload nodes UI side when off screen offset by max drag distance
|
||||
# y = viewport.get('y')
|
||||
nodes: list = await graph.V()\
|
||||
.valueMap(True).toList()
|
||||
# .has('y', P.between(y, y2))\
|
||||
# \
|
||||
# .has('y', P.lt(y)).has('y', P.gt(y_offset))\
|
||||
# .has('x', P.gt(x))\
|
||||
# .has('x', P.between(x1, x2))\
|
||||
# .has('y', P.between(y1, y2))\
|
||||
# .or_(_g.has('x', P.gt(x1)), _g.has('x', P.lt(x2)))
|
||||
# maybe something like the above?!
|
||||
|
||||
return nodes, edges
|
||||
|
||||
|
||||
@@ -149,10 +155,13 @@ def node_to_blueprint_entity(ui_entity_element, node) -> None:
|
||||
if entity_label not in k:
|
||||
ui_entity_element[k] = node[v][0]
|
||||
|
||||
async def read_graph(action_type, send_json, project_uuid):
|
||||
|
||||
async def read_graph(viewport_event, send_json, project_uuid, is_initial_read: bool = False):
|
||||
nodes = []
|
||||
edges_data = []
|
||||
data_nodes, edges = await load_initial_graph(project_uuid)
|
||||
data_nodes, edges = await load_nodes_from_db(project_uuid, viewport_event)
|
||||
if is_initial_read:
|
||||
EntityRegistry.discover_plugins()
|
||||
tmp_invalid_fix = []
|
||||
tmp_invalid_id_fix = []
|
||||
for node in data_nodes:
|
||||
@@ -162,9 +171,9 @@ async def read_graph(action_type, send_json, project_uuid):
|
||||
}
|
||||
entity_id = node.pop(T.id)
|
||||
entity_type = node.pop(T.label)
|
||||
plugin = await Registry.get_plugin(to_snake_case(entity_type))
|
||||
plugin = await EntityRegistry.get_plugin(to_snake_case(entity_type))
|
||||
if plugin:
|
||||
ui_entity = plugin.blueprint()
|
||||
ui_entity = plugin.create()
|
||||
for element in ui_entity['elements']:
|
||||
if isinstance(element, list):
|
||||
[node_to_blueprint_entity(
|
||||
@@ -187,8 +196,8 @@ async def read_graph(action_type, send_json, project_uuid):
|
||||
}
|
||||
nodes.append(ui_entity)
|
||||
else:
|
||||
tmp_invalid_id_fix.append(entity_id)
|
||||
# TODO detect invalid/renamed entities/plugins on any plugin label updates and fix graph
|
||||
tmp_invalid_id_fix.append(entity_id)
|
||||
tmp_invalid_fix.append(entity_type)
|
||||
print(f'(TODO: on entity code update and label change rename nodes in janusgraph) Error Invalid Plugin: {entity_type}')
|
||||
if len(edges[0]) > 0:
|
||||
@@ -207,22 +216,24 @@ async def read_graph(action_type, send_json, project_uuid):
|
||||
'type': 'float'
|
||||
})
|
||||
await send_json({
|
||||
'action': 'isInitialRead',
|
||||
'action': 'isInitialRead' if is_initial_read else 'read',
|
||||
'nodes': nodes,
|
||||
'edges': edges_data
|
||||
})
|
||||
|
||||
|
||||
async def update_node(node, action_type, send_json, uuid: UUID):
|
||||
async def update_node(node, send_json, uuid: UUID):
|
||||
if updateTargetId := node.pop('id', None):
|
||||
async with ProjectGraphConnection(uuid) as graph:
|
||||
updateTarget = graph.V(updateTargetId)
|
||||
for k, v in node.items():
|
||||
# if k == 'x' or k == 'y':
|
||||
# print(to_snake_case(k), v)
|
||||
updateTarget.property(Cardinality.single, to_snake_case(k), v)
|
||||
await updateTarget.next()
|
||||
|
||||
|
||||
async def remove_nodes(node, action_type, send_json, uuid: UUID):
|
||||
async def remove_nodes(node, send_json, uuid: UUID):
|
||||
async with ProjectGraphConnection(uuid) as graph:
|
||||
if targetNode := node.get('id'):
|
||||
await graph.V(targetNode).drop().next()
|
||||
@@ -243,23 +254,20 @@ async def get_transform_notification(transform_output, transform_type):
|
||||
|
||||
|
||||
async def nodes_transform(
|
||||
node: dict,
|
||||
entity_context: dict,
|
||||
send_json: Callable[[dict], None],
|
||||
uuid: UUID
|
||||
):
|
||||
transform_result = {}
|
||||
entity_type = node.get('data', {}).get('label')
|
||||
transform_type = None
|
||||
plugin = await Registry.get_plugin(entity_type)
|
||||
transform_type = entity_context.get("transform")
|
||||
entity_data = entity_context.get("data", {})
|
||||
entity_type = entity_data.get("label")
|
||||
plugin = await EntityRegistry.get_plugin(entity_type)
|
||||
if plugin := plugin():
|
||||
transform_type = node["transform"]
|
||||
transform_result = await plugin.get_transform(
|
||||
transform_result = await plugin.run_transform(
|
||||
transform_type=transform_type,
|
||||
entity=node,
|
||||
use=PluginUse(
|
||||
get_driver=deps.get_driver,
|
||||
get_graph=lambda: None
|
||||
)
|
||||
transform_context=entity_context,
|
||||
use=TransformUse(get_driver=deps.get_driver, config=EntityConfig(enabled=False, test=1, test2="two"))
|
||||
)
|
||||
async def create_entity_data(
|
||||
graph: AsyncGraphTraversal,
|
||||
@@ -298,13 +306,15 @@ async def nodes_transform(
|
||||
|
||||
async with ProjectGraphConnection(uuid) as graph:
|
||||
if isinstance(transform_result, list):
|
||||
out_result = [await create_entity_data(graph, result, node) for result in transform_result]
|
||||
out_result = [await create_entity_data(graph, result, entity_context) for result in transform_result]
|
||||
else:
|
||||
out_result = list(await create_entity_data(graph, transform_result, node))
|
||||
out_result = list(await create_entity_data(graph, transform_result, entity_context))
|
||||
transform_notification = await get_transform_notification(transform_result, transform_type)
|
||||
|
||||
await send_json({
|
||||
"action": "isLoading",
|
||||
"detail": False,
|
||||
"message": await get_transform_notification(transform_result, transform_type)
|
||||
"message": transform_notification
|
||||
})
|
||||
await send_json({
|
||||
"action": "createEntity",
|
||||
@@ -314,101 +324,115 @@ async def nodes_transform(
|
||||
await send_json({'error': 'noPluginFound'})
|
||||
|
||||
|
||||
async def get_command_type(event):
|
||||
async def run_user_graph_event(event: dict, send_json: Callable, uuid: UUID) -> None:
|
||||
user_event: list = event["action"].split(":")
|
||||
action = user_event[0]
|
||||
USER_ACTION = None
|
||||
if len(user_event) >= 2:
|
||||
USER_ACTION = user_event[1]
|
||||
IS_READ = action == "read"
|
||||
IS_UPDATE = action == "update"
|
||||
IS_DELETE = action == "delete"
|
||||
IS_TRANSFORM = action == "transform"
|
||||
return USER_ACTION, IS_READ, IS_UPDATE, IS_DELETE, IS_TRANSFORM
|
||||
IS_READ: bool = action == "read"
|
||||
IS_UPDATE: bool = action == "update"
|
||||
IS_DELETE: bool = action == "delete"
|
||||
IS_TRANSFORM: bool = action == "transform"
|
||||
IS_INITIAL: bool = 'initial' in action
|
||||
|
||||
ACTION_TARGET = user_event[1]
|
||||
|
||||
async def run_user_graph_event(event: dict, send_json: Callable, uuid: UUID) -> None:
|
||||
USER_ACTION, IS_READ, IS_UPDATE, IS_DELETE, IS_TRANSFORM = await get_command_type(event)
|
||||
if USER_ACTION == 'node':
|
||||
if IS_READ:
|
||||
await send_json({"action": "isLoading", "detail": True })
|
||||
await read_graph(USER_ACTION, send_json, uuid)
|
||||
await send_json({ "action": "isLoading", "detail": False, "message": "Success! Your graph environment has loaded!" })
|
||||
if ACTION_TARGET == 'node':
|
||||
if IS_UPDATE:
|
||||
await update_node(event["node"], USER_ACTION, send_json, uuid)
|
||||
await update_node(event["node"], send_json, uuid)
|
||||
if IS_DELETE:
|
||||
await remove_nodes(event["node"], USER_ACTION, send_json, uuid)
|
||||
await remove_nodes(event["node"], send_json, uuid)
|
||||
if IS_TRANSFORM:
|
||||
await send_json({"action": "isLoading", "detail": True })
|
||||
await nodes_transform(event["node"], send_json, uuid)
|
||||
if ACTION_TARGET == 'graph':
|
||||
if IS_READ:
|
||||
await read_graph(event["viewport"], send_json, uuid)
|
||||
await send_json({ "action": "isLoading", "detail": False, "message": "Success! You've been reconnected!" })
|
||||
if IS_INITIAL:
|
||||
await send_json({"action": "isLoading", "detail": True })
|
||||
await read_graph(event["viewport"], send_json, uuid, True)
|
||||
await send_json({ "action": "isLoading", "detail": False, "message": "Success! Your graph environment has loaded!" })
|
||||
|
||||
|
||||
# TODO: Finish implementing 'multiplayer' logic
|
||||
graph_users: Dict[str, WebSocket] = {}
|
||||
|
||||
@router.websocket("/graph/{hid}")
|
||||
async def active_graph_inquiry(
|
||||
websocket: WebSocket,
|
||||
user: Annotated[schemas.User, Depends(deps.get_user_from_ws)],
|
||||
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_ws)],
|
||||
hid: Annotated[int, Depends(deps.get_graph_id)],
|
||||
db: Session = Depends(deps.get_db)
|
||||
):
|
||||
await websocket.accept()
|
||||
is_project_active = True
|
||||
EntityRegistry.discover_plugins()
|
||||
graph_users[user.cid.hex] = websocket
|
||||
active_inquiry = crud.graphs.get(db, id=hid)
|
||||
if active_inquiry is None:
|
||||
is_project_active = False
|
||||
else:
|
||||
await websocket.send_json({"action": "isLoading", "detail": True })
|
||||
|
||||
while is_project_active:
|
||||
await websocket.accept()
|
||||
if active_inquiry is not None:
|
||||
await graph_users[user.cid.hex].send_json({"action": "isLoading", "detail": True })
|
||||
else:
|
||||
websocket.send({"action": "error"})
|
||||
|
||||
for user_cid, ws in graph_users.items():
|
||||
try:
|
||||
event: dict = await websocket.receive_json()
|
||||
await run_user_graph_event(
|
||||
event=event,
|
||||
send_json=websocket.send_json,
|
||||
uuid=active_inquiry.uuid
|
||||
)
|
||||
async for event in ws.iter_json():
|
||||
await run_user_graph_event(
|
||||
event=event,
|
||||
send_json=ws.send_json,
|
||||
uuid=active_inquiry.uuid
|
||||
)
|
||||
except OBPluginError as e:
|
||||
await websocket.send_json({"action": "error", "detail": f"{e}"})
|
||||
await websocket.send_json({"action": "isLoading", "detail": False })
|
||||
await ws.send_json({"action": "error", "detail": f"{e}"})
|
||||
await ws.send_json({"action": "isLoading", "detail": False })
|
||||
log.error(e)
|
||||
except (WebSocketException, BufferError, ConnectionClosedError) as e:
|
||||
await websocket.send_json({"action": "isLoading", "detail": False })
|
||||
except (WebSocketException, ConnectionClosedError) as e:
|
||||
log.error("Exception inside node.active_project")
|
||||
log.error(e)
|
||||
is_project_active = False
|
||||
await websocket.close()
|
||||
except WebSocketDisconnect as e:
|
||||
log.info(f"disconnected!")
|
||||
await ws.send_json({"action": "isLoading", "detail": False })
|
||||
await ws.close()
|
||||
# del graph_users[user_cid]
|
||||
except (WebSocketDisconnect, RuntimeError) as e:
|
||||
if isinstance(e, RuntimeError):
|
||||
log.error(e)
|
||||
else:
|
||||
log.info((
|
||||
f"disconnect user -> {user_cid} from all"
|
||||
f"http://localhost:3000/graph/inquiry/{hid} users: {graph_users}"
|
||||
))
|
||||
del graph_users[user_cid]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@router.get("/refresh")
|
||||
async def refresh_plugins(
|
||||
async def refresh_entity_plugins(
|
||||
hid: Annotated[str, Depends(deps.get_graph_id)],
|
||||
user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
|
||||
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)],
|
||||
db: Session = Depends(deps.get_db)
|
||||
):
|
||||
try:
|
||||
refresh_local_entities(db)
|
||||
return {"status": "success", "plugins": Registry.ui_labels}
|
||||
except Exception as e:
|
||||
log.error("Error inside node.refresh_plugins")
|
||||
log.error(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="There was an error refreshing, please try again."
|
||||
)
|
||||
# try:
|
||||
EntityRegistry.discover_plugins()
|
||||
return {"status": "success", "plugins": EntityRegistry._visible_entities}
|
||||
# except Exception as e:
|
||||
# log.error("Error inside node.refresh_plugins")
|
||||
# log.error(e)
|
||||
# raise HTTPException(
|
||||
# status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
# detail="There was an error refreshing, please try again."
|
||||
# )
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_entity_on_drop(
|
||||
hid: Annotated[str, Depends(deps.get_graph_id)],
|
||||
user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
|
||||
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)],
|
||||
create_node: schemas.CreateNode,
|
||||
db: Session = Depends(deps.get_db)
|
||||
):
|
||||
try:
|
||||
active_inquiry = crud.graphs.get(db, id=hid)
|
||||
plugin = await Registry.get_plugin(plugin_label=to_snake_case(create_node.label))
|
||||
plugin = await EntityRegistry.get_plugin(plugin_label=to_snake_case(create_node.label))
|
||||
if plugin:
|
||||
blueprint = plugin.blueprint()
|
||||
blueprint = plugin.create()
|
||||
blueprint["position"] = create_node.position.model_dump()
|
||||
return await save_node_on_drop(
|
||||
create_node.label,
|
||||
@@ -1,9 +1,12 @@
|
||||
import datetime
|
||||
from typing import Generator, Any
|
||||
from contextlib import contextmanager
|
||||
import uuid
|
||||
|
||||
import boto3
|
||||
from botocore.exceptions import BotoCoreError
|
||||
from fastapi import Depends, HTTPException, WebSocket, WebSocketException
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from sqlalchemy.orm import Session
|
||||
import undetected_chromedriver as uc
|
||||
from selenium import webdriver
|
||||
@@ -13,13 +16,16 @@ from selenium.webdriver.chrome.webdriver import WebDriver
|
||||
from app import schemas, crud
|
||||
from app.db.session import SessionLocal
|
||||
from app.core.logger import get_logger
|
||||
from app.api.utils import APIRequest, HidChecker, hid
|
||||
from app.api.utils import APIRequest, HidChecker, get_hid
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
log = get_logger("api.deps")
|
||||
|
||||
|
||||
if "dev" in settings.ENVIRONMENT:
|
||||
___OB_DEV_CID___ = uuid.uuid4()
|
||||
|
||||
def get_db() -> Generator[Any, Session, Any]:
|
||||
try:
|
||||
db: Session = SessionLocal()
|
||||
@@ -31,26 +37,56 @@ def get_db() -> Generator[Any, Session, Any]:
|
||||
async def get_user_from_session(
|
||||
request: APIRequest,
|
||||
db: Session = Depends(get_db)
|
||||
) -> schemas.User:
|
||||
if 'dev' in settings.ENVIRONMENT:
|
||||
return {}
|
||||
) -> schemas.UserInDBBase:
|
||||
|
||||
if user := request.session.get("member"):
|
||||
ob_user: schemas.User | None = crud.user.get_by_cid(db=db, cid=user.get("id"))
|
||||
ob_user: schemas.UserInDBBase | None = crud.user.get_by_cid(db=db, cid=user.get("id"))
|
||||
if ob_user:
|
||||
return ob_user
|
||||
ob_user = jsonable_encoder(ob_user)
|
||||
return schemas.UserInDBBase(**ob_user)
|
||||
if "dev" in settings.ENVIRONMENT:
|
||||
return schemas.UserCreate(
|
||||
cid=___OB_DEV_CID___,
|
||||
name="DEV",
|
||||
username="DEV",
|
||||
email="dev@osintbuddy.com",
|
||||
avatar="",
|
||||
phone="",
|
||||
display_name="Dev Account",
|
||||
first_name="Dev",
|
||||
last_name="eloper",
|
||||
is_admin=True,
|
||||
created_time=datetime.datetime.now(),
|
||||
updated_time=datetime.datetime.now(),
|
||||
)
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
|
||||
|
||||
async def get_user_from_ws(
|
||||
websocket: WebSocket,
|
||||
db: Session = Depends(get_db)
|
||||
) -> schemas.User:
|
||||
if 'dev' in settings.ENVIRONMENT:
|
||||
return {}
|
||||
) -> schemas.UserInDBBase:
|
||||
|
||||
if user := websocket.session.get("member"):
|
||||
ob_user: schemas.User | None = crud.user.get_by_cid(db=db, cid=user.get("id"))
|
||||
ob_user: schemas.UserInDBBase | None = crud.user.get_by_cid(db=db, cid=user.get("id"))
|
||||
if ob_user:
|
||||
return ob_user
|
||||
ob_user = jsonable_encoder(ob_user)
|
||||
return schemas.UserInDBBase(**ob_user)
|
||||
if "dev" in settings.ENVIRONMENT:
|
||||
return schemas.UserInDBBase(
|
||||
cid=___OB_DEV_CID___,
|
||||
name="DEV",
|
||||
username="DEV",
|
||||
email="dev@osintbuddy.com",
|
||||
avatar="",
|
||||
phone="",
|
||||
display_name="Dev Account",
|
||||
first_name="Dev",
|
||||
last_name="eloper",
|
||||
is_admin=True,
|
||||
created_time=datetime.datetime.now(),
|
||||
updated_time=datetime.datetime.now(),
|
||||
)
|
||||
raise WebSocketException(code=401)
|
||||
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ if TYPE_CHECKING:
|
||||
sqids = Sqids(alphabet=settings.SQIDS_ALPHABET, min_length=4)
|
||||
|
||||
|
||||
def hid(db_id: int, ns: int = settings.SQIDS_NAMESPACE) -> str:
|
||||
def get_hid(db_id: int, ns: int = settings.SQIDS_NAMESPACE) -> str:
|
||||
"""Generate an ID thats easy for humans to work with"""
|
||||
return sqids.encode([ns, db_id])
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, select
|
||||
|
||||
@@ -31,6 +32,11 @@ class CRUDEntities(CRUDBase[
|
||||
) -> List[ModelType]:
|
||||
return db.query(self.model).where(self.model.uuid == uuid).first()
|
||||
|
||||
def get_by_label(
|
||||
self, db: Session, *, label: str = None
|
||||
) -> List[ModelType]:
|
||||
return db.query(self.model).where(self.model.label == label).first()
|
||||
|
||||
def update_favorite_by_id(self, db: Session, db_obj: Entities, is_favorite: bool = False):
|
||||
setattr(db_obj, 'is_favorite', is_favorite)
|
||||
db.add(db_obj)
|
||||
|
||||
@@ -45,7 +45,7 @@ class CRUDGraphs(CRUDBase[
|
||||
def get_many_user_graphs(
|
||||
self,
|
||||
db: Session,
|
||||
user: schemas.User,
|
||||
user: schemas.UserInDBBase,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
is_favorite: bool = True
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import inspect, time, importlib, imp
|
||||
|
||||
import requests
|
||||
from sqlalchemy.orm import Session
|
||||
from osintbuddy import load_plugin, Registry
|
||||
from osintbuddy.plugins import EntityRegistry
|
||||
|
||||
from app import crud, schemas
|
||||
from app.core.logger import get_logger
|
||||
@@ -9,56 +11,61 @@ from .base import * # noqa
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
core_ob_url = 'https://raw.githubusercontent.com/jerlendds/osintbuddy-core-plugins/main/plugins/'
|
||||
core_plugins = {
|
||||
'CSE Result': 'cse_result',
|
||||
'CSE Search': 'cse_search',
|
||||
'DNS': 'dns',
|
||||
'Google Cache Result': 'google_cache_result',
|
||||
'Google Cache Search': 'google_cache_search',
|
||||
'Google Result': 'google_result',
|
||||
'Google Search': 'google_search',
|
||||
'IP': 'ip',
|
||||
'IP Geolocation': 'ip_geolocation',
|
||||
'Subdomain': 'subdomain',
|
||||
'Telegram Websearch': 'telegram_websearch',
|
||||
'URL': 'url',
|
||||
'Username': 'username',
|
||||
'Username Profile': 'username_profile',
|
||||
'Website': 'website',
|
||||
'Whois': 'whois'
|
||||
}
|
||||
core_ob_url = 'https://raw.githubusercontent.com/jerlendds/osintbuddy-core-plugins/develop/plugins/'
|
||||
core_entity_files: list[str] = [
|
||||
'google_cse_result',
|
||||
'google_cse_search',
|
||||
'dns',
|
||||
'google_cache_result',
|
||||
'google_cache_search',
|
||||
'google_result',
|
||||
'google_search',
|
||||
'ip',
|
||||
'ip_geolocation',
|
||||
'subdomain',
|
||||
'telegram_cse_search',
|
||||
'url',
|
||||
'username',
|
||||
'username_profile',
|
||||
'website',
|
||||
'whois'
|
||||
]
|
||||
|
||||
def load_initial_plugin(db, plugin_mod, plugin_code):
|
||||
load_plugin(plugin_mod, plugin_code)
|
||||
|
||||
plugin = Registry.get_plug(plugin_mod)
|
||||
obj_in = schemas.EntityCreate(
|
||||
label=plugin.label,
|
||||
author=plugin.author,
|
||||
description=plugin.description,
|
||||
source=plugin_code,
|
||||
is_favorite=False
|
||||
)
|
||||
return crud.entities.create(db=db, obj_in=obj_in)
|
||||
|
||||
def init_db(db: Session) -> None:
|
||||
entity_count = crud.entities.count_all(db)[0][0]
|
||||
if entity_count < 14:
|
||||
for plugin_label, plugin_mod in core_plugins.items():
|
||||
log.info(f'Loading core plugins: {plugin_label}')
|
||||
for file_endpoint in core_entity_files:
|
||||
log.info(f"Saving core plugin/entity file: {file_endpoint}")
|
||||
try:
|
||||
resp = requests.get(core_ob_url + plugin_mod + '.py')
|
||||
load_initial_plugin(db=db, plugin_mod=plugin_mod, plugin_code=resp.text)
|
||||
resp = requests.get(core_ob_url + file_endpoint + ".py")
|
||||
source_file = open(f"/app/app/plugins/{file_endpoint}.py", "w+")
|
||||
source_file.write(resp.text)
|
||||
source_file.close()
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
log.error(e)
|
||||
resp = requests.get(core_ob_url + plugin_mod + '.py')
|
||||
load_initial_plugin(db=db, plugin_mod=plugin_mod, plugin_code=resp.text)
|
||||
except Exception:
|
||||
pass
|
||||
# TODO: Use tenacity lib retry logic instead
|
||||
log.error("Error loading core plugin! ", e)
|
||||
|
||||
EntityRegistry.discover_plugins()
|
||||
for e in EntityRegistry.entities:
|
||||
log.info(f"Loading core plugin/entity: {e.label}")
|
||||
entity_file = open(inspect.getsourcefile(e), "r")
|
||||
obj_in = schemas.EntityCreate(
|
||||
label=e.label,
|
||||
author=e.author,
|
||||
description=e.description,
|
||||
source=entity_file.read(),
|
||||
is_favorite=False
|
||||
)
|
||||
entity_file.close()
|
||||
entity_obj = crud.entities.get_by_label(db=db, label=obj_in.label)
|
||||
if entity_obj:
|
||||
crud.entities.update(db, db_obj=entity_obj, obj_in=obj_in.model_dump())
|
||||
else:
|
||||
crud.entities.create(db, obj_in=obj_in)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"service": "[Entities: Create]",
|
||||
"message": f"{len(Registry.plugins)} initial entities (plugins) loaded",
|
||||
"message": f"{len(EntityRegistry.entities)} initial entities (plugins) loaded",
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
|
||||
import os
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True)
|
||||
|
||||
DB_POOL_SIZE = int(os.getenv("DB_POOL_SIZE", "100"))
|
||||
WEB_CONCURRENCY = int(os.getenv("WEB_CONCURRENCY", "4"))
|
||||
POOL_SIZE = max(DB_POOL_SIZE // WEB_CONCURRENCY, 5)
|
||||
|
||||
engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, pool_size=POOL_SIZE, max_overflow=0, pool_pre_ping=True)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<mjml>
|
||||
<mj-body background-color="#fff">
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-divider border-color="#555"></mj-divider>
|
||||
<mj-text font-size="20px" color="#555" font-family="helvetica">{{ project_name }} - New Account</mj-text>
|
||||
<mj-text font-size="16px" color="#555">You have a new account:</mj-text>
|
||||
<mj-text font-size="16px" color="#555">Username: {{ username }}</mj-text>
|
||||
<mj-text font-size="16px" color="#555">Password: {{ password }}</mj-text>
|
||||
<mj-button padding="50px 0px" href="{{ link }}">Go to Dashboard</mj-button>
|
||||
<mj-divider border-color="#555" border-width="2px" />
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -1,19 +0,0 @@
|
||||
<mjml>
|
||||
<mj-body background-color="#fff">
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-divider border-color="#555"></mj-divider>
|
||||
<mj-text font-size="20px" color="#555" font-family="helvetica">{{ project_name }} - Password Recovery</mj-text>
|
||||
<mj-text font-size="16px" color="#555">We received a request to recover the password for user {{ username }}
|
||||
with email {{ email }}</mj-text>
|
||||
<mj-text font-size="16px" color="#555">Reset your password by clicking the button below:</mj-text>
|
||||
<mj-button padding="50px 0px" href="{{ link }}">Reset Password</mj-button>
|
||||
<mj-text font-size="16px" color="#555">Or open the following link:</mj-text>
|
||||
<mj-text font-size="16px" color="#555"><a href="{{ link }}">{{ link }}</a></mj-text>
|
||||
<mj-divider border-color="#555" border-width="2px" />
|
||||
<mj-text font-size="14px" color="#555">The reset password link / button will expire in {{ valid_hours }} hours.</mj-text>
|
||||
<mj-text font-size="14px" color="#555">If you didn't request a password recovery you can disregard this email.</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -1,11 +0,0 @@
|
||||
<mjml>
|
||||
<mj-body background-color="#fff">
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-divider border-color="#555"></mj-divider>
|
||||
<mj-text font-size="20px" color="#555" font-family="helvetica">{{ project_name }}</mj-text>
|
||||
<mj-text font-size="16px" color="#555">Test email for: {{ email }}</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -14,12 +14,12 @@ def init() -> None:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
try:
|
||||
log.info("Running initial data check...")
|
||||
init()
|
||||
log.info("Initial data check completed.")
|
||||
except Exception as e:
|
||||
log.error(e)
|
||||
# try:
|
||||
log.info("Running initial data check...")
|
||||
init()
|
||||
log.info("Initial data check completed.")
|
||||
# except Exception as e:
|
||||
# log.error(e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -8,7 +8,7 @@ from starlette.middleware.sessions import SessionMiddleware
|
||||
# from fastapi_cache.backends.redis import CACHE_KEY, RedisCacheBackend
|
||||
# import sentry_sdk
|
||||
# from sentry_sdk.integrations.asyncio import AsyncioIntegration
|
||||
from app.api.api_v1.api import api_router, node
|
||||
from app.api.api_v1.api import api_router, graph_inquiry
|
||||
from app.core.config import settings, use_route_names_as_operation_ids
|
||||
from app.core.casdoor import Config as CasdoorConfig
|
||||
# if settings.SENTRY_DSN:
|
||||
|
||||
@@ -3,7 +3,7 @@ from uuid import UUID
|
||||
from typing import Optional, List
|
||||
|
||||
from pydantic import BaseModel, validator, ConfigDict
|
||||
from app.api.utils import hid
|
||||
from app.api.utils import get_hid
|
||||
from osintbuddy.templates import plugin_source_template
|
||||
|
||||
ENTITY_NAMESPACE = 1510
|
||||
@@ -35,10 +35,10 @@ class PostEntityCreate(BaseModel):
|
||||
|
||||
|
||||
class EntityUpdate(EntityBase):
|
||||
label: Optional[str]
|
||||
author: Optional[str]
|
||||
description: Optional[str]
|
||||
source: Optional[str]
|
||||
label: Optional[str] = ""
|
||||
author: Optional[str] = ""
|
||||
description: Optional[str] = ""
|
||||
source: Optional[str]
|
||||
|
||||
|
||||
class EntityInDBBase(EntityBase):
|
||||
@@ -47,7 +47,7 @@ class EntityInDBBase(EntityBase):
|
||||
'id',
|
||||
pre=True,
|
||||
allow_reuse=True
|
||||
)(lambda v: hid(v, ENTITY_NAMESPACE))
|
||||
)(lambda v: get_hid(v, ENTITY_NAMESPACE))
|
||||
|
||||
last_edited: datetime.datetime
|
||||
updated: datetime.datetime
|
||||
@@ -55,7 +55,8 @@ class EntityInDBBase(EntityBase):
|
||||
|
||||
|
||||
class Entity(EntityInDBBase):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
# model_config = ConfigDict(from_attributes=True)
|
||||
pass
|
||||
|
||||
|
||||
class EntityInList(PostEntityCreate):
|
||||
|
||||
@@ -3,7 +3,7 @@ from uuid import UUID
|
||||
from typing import Optional, List
|
||||
|
||||
from pydantic import BaseModel, validator
|
||||
from app.api.utils import hid, sqids
|
||||
from app.api.utils import get_hid, sqids
|
||||
|
||||
GRAPH_NAMESPACE = 5172
|
||||
|
||||
@@ -31,7 +31,7 @@ class GraphInDBBase(GraphsBase):
|
||||
'id',
|
||||
pre=True,
|
||||
allow_reuse=True
|
||||
)(lambda v: hid(v, GRAPH_NAMESPACE))
|
||||
)(lambda v: get_hid(v, GRAPH_NAMESPACE))
|
||||
|
||||
class Config:
|
||||
from_orm = True
|
||||
|
||||
@@ -72,9 +72,6 @@ class User(UserCreate):
|
||||
|
||||
|
||||
class UserInDBBase(UserCreate):
|
||||
is_admin: bool
|
||||
created_time: datetime.datetime
|
||||
updated_time: datetime.datetime
|
||||
cid: UUID
|
||||
|
||||
class Config:
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import osintbuddy
|
||||
import osintbuddy as ob
|
||||
from osintbuddy import load_local_plugins
|
||||
from app import crud, schemas, models
|
||||
from app.core.logger import get_logger
|
||||
from app.db.session import SessionLocal
|
||||
|
||||
from termcolor import colored
|
||||
from pyfiglet import figlet_format
|
||||
|
||||
db = SessionLocal()
|
||||
entities = crud.entities.get_many(db, skip=0, limit=100)
|
||||
load_local_plugins(entities, "plugins")
|
||||
|
||||
print(
|
||||
colored(figlet_format("osintbuddy py >", font='smslant',), color="green"),
|
||||
colored("Created by jerlendds", color="blue"),
|
||||
@@ -12,6 +18,11 @@ print(
|
||||
###############################################################################
|
||||
# The following imports and variables have been included in your current shell:
|
||||
###############################################################################
|
||||
import osintbuddy
|
||||
import osintbuddy as ob
|
||||
from osintbuddy import load_local_plugins
|
||||
from app import crud, schemas, models
|
||||
from app.db.session import SessionLocal
|
||||
db = SessionLocal()
|
||||
entities = crud.entities.get_many(db, skip=0, limit=100)
|
||||
load_local_plugins(entities, 'plugins')
|
||||
""", color="green"))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.11.4-slim-bullseye
|
||||
FROM python:3.11.7-slim-bullseye
|
||||
LABEL maintainer="jerlendds <support@forum.osintbuddy.com>"
|
||||
|
||||
WORKDIR /app/
|
||||
|
||||
@@ -28,7 +28,8 @@ watchfiles==0.19.0
|
||||
|
||||
gremlinpy @ git+https://github.com/jerlendds/gremlinpy.git@eaba7dca12ad0156eb0d6d8ba2eb5751551c6a6d
|
||||
|
||||
warcio==1.7.4
|
||||
sqids==0.4.0
|
||||
sentry-sdk==1.33.1
|
||||
sentry-sdk==1.39.1
|
||||
boto3-stubs==1.28.75
|
||||
|
||||
|
||||
1
backend/plugins/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.py
|
||||
@@ -28,5 +28,6 @@ watchfiles==0.19.0
|
||||
|
||||
gremlinpy @ git+https://github.com/jerlendds/gremlinpy.git@eaba7dca12ad0156eb0d6d8ba2eb5751551c6a6d
|
||||
|
||||
warcio==1.7.4
|
||||
sqids==0.4.0
|
||||
sentry-sdk==1.33.1
|
||||
sentry-sdk==1.39.1
|
||||
@@ -3,9 +3,6 @@
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@blocksuite/blocks": "^0.10.0",
|
||||
"@blocksuite/editor": "^0.0.0-20231129200023-14f7d22b-nightly",
|
||||
"@blocksuite/store": "^0.10.0",
|
||||
"@codemirror/lang-python": "^6.1.3",
|
||||
"@headlessui/react": "^1.7.7",
|
||||
"@heroicons/react": "^2.0.13",
|
||||
@@ -13,6 +10,7 @@
|
||||
"@reactour/tour": "^3.6.1",
|
||||
"@reduxjs/toolkit": "^1.9.1",
|
||||
"@tabler/icons-react": "^2.42.0",
|
||||
"@tarii/3d-earth": "^1.0.3",
|
||||
"@uiw/codemirror-theme-aura": "^4.21.20",
|
||||
"@uiw/react-codemirror": "^4.21.20",
|
||||
"casdoor-js-sdk": "^0.11.0",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { emptyApi as api } from "./baseApi";
|
||||
const injectedRtkApi = api.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
refreshPlugins: build.query<
|
||||
RefreshPluginsApiResponse,
|
||||
RefreshPluginsApiArg
|
||||
refreshEntityPlugins: build.query<
|
||||
RefreshEntityPluginsApiResponse,
|
||||
RefreshEntityPluginsApiArg
|
||||
>({
|
||||
query: (queryArg) => ({
|
||||
url: `/api/v1/node/refresh`,
|
||||
@@ -125,7 +125,7 @@ const injectedRtkApi = api.injectEndpoints({
|
||||
query: (queryArg) => ({
|
||||
url: `/api/v1/entity/${queryArg.hid}`,
|
||||
method: "PUT",
|
||||
body: queryArg.entityBase,
|
||||
body: queryArg.entityUpdate,
|
||||
}),
|
||||
}),
|
||||
deleteEntity: build.mutation<DeleteEntityApiResponse, DeleteEntityApiArg>({
|
||||
@@ -176,9 +176,9 @@ const injectedRtkApi = api.injectEndpoints({
|
||||
overrideExisting: false,
|
||||
});
|
||||
export { injectedRtkApi as api };
|
||||
export type RefreshPluginsApiResponse =
|
||||
export type RefreshEntityPluginsApiResponse =
|
||||
/** status 200 Successful Response */ any;
|
||||
export type RefreshPluginsApiArg = {
|
||||
export type RefreshEntityPluginsApiArg = {
|
||||
hid: string;
|
||||
};
|
||||
export type CreateEntityOnDropApiResponse =
|
||||
@@ -200,7 +200,7 @@ export type PostSignoutApiResponse =
|
||||
/** status 200 Successful Response */ Status;
|
||||
export type PostSignoutApiArg = void;
|
||||
export type GetAccountApiResponse = /** status 200 Successful Response */
|
||||
| User
|
||||
| UserInDbBase
|
||||
| HttpError;
|
||||
export type GetAccountApiArg = void;
|
||||
export type GetGraphApiResponse = /** status 200 Successful Response */ Graph;
|
||||
@@ -264,7 +264,7 @@ export type UpdateEntityByIdApiResponse =
|
||||
/** status 200 Successful Response */ any;
|
||||
export type UpdateEntityByIdApiArg = {
|
||||
hid: string;
|
||||
entityBase: EntityBase;
|
||||
entityUpdate: EntityUpdate;
|
||||
};
|
||||
export type DeleteEntityApiResponse = /** status 200 Successful Response */ any;
|
||||
export type DeleteEntityApiArg = {
|
||||
@@ -313,7 +313,7 @@ export type Status = {
|
||||
export type HttpError = {
|
||||
detail: string;
|
||||
};
|
||||
export type User = {
|
||||
export type UserInDbBase = {
|
||||
name: string;
|
||||
username?: string | null;
|
||||
email?: string | null;
|
||||
@@ -325,7 +325,7 @@ export type User = {
|
||||
is_admin: boolean;
|
||||
created_time: string;
|
||||
updated_time: string;
|
||||
uuid: string;
|
||||
cid: string;
|
||||
};
|
||||
export type Graph = {
|
||||
label: string;
|
||||
@@ -381,11 +381,11 @@ export type PostEntityCreate = {
|
||||
author: string;
|
||||
description: string;
|
||||
};
|
||||
export type EntityBase = {
|
||||
label?: string;
|
||||
author?: string;
|
||||
description?: string;
|
||||
source?: string;
|
||||
export type EntityUpdate = {
|
||||
label?: string | null;
|
||||
author?: string | null;
|
||||
description?: string | null;
|
||||
source: string | null;
|
||||
is_favorite?: boolean;
|
||||
};
|
||||
export type ScanMachineCreate = {
|
||||
@@ -393,7 +393,7 @@ export type ScanMachineCreate = {
|
||||
description: string;
|
||||
};
|
||||
export const {
|
||||
useRefreshPluginsQuery,
|
||||
useRefreshEntityPluginsQuery,
|
||||
useCreateEntityOnDropMutation,
|
||||
useGetCasdoorConfigQuery,
|
||||
usePostSigninMutation,
|
||||
|
||||
BIN
frontend/src/assets/images/icg-gh.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
1
frontend/src/assets/images/icg-text.svg
Normal file
|
After Width: | Height: | Size: 19 KiB |
254
frontend/src/assets/images/icg.svg
Normal file
@@ -0,0 +1,254 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
height="69.776581mm"
|
||||
viewBox="0 0 218.55358 69.776582"
|
||||
width="218.55359mm"
|
||||
version="1.1"
|
||||
id="svg30"
|
||||
sodipodi:docname="OSINTBuddy.svg"
|
||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs30" />
|
||||
<sodipodi:namedview
|
||||
id="namedview30"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="0.93338092"
|
||||
inkscape:cx="413.01466"
|
||||
inkscape:cy="131.779"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1024"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg30" />
|
||||
<linearGradient
|
||||
id="a">
|
||||
<stop
|
||||
offset="0"
|
||||
stop-color="#b0bccf"
|
||||
id="stop1" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#b0bccf"
|
||||
stop-opacity="0"
|
||||
id="stop2" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="b"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="230.540335"
|
||||
x2="369.067145"
|
||||
xlink:href="#a"
|
||||
y1="409.54838"
|
||||
y2="409.54838" />
|
||||
<linearGradient
|
||||
id="c"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="248.126665"
|
||||
x2="386.653475"
|
||||
xlink:href="#a"
|
||||
y1="367.26637"
|
||||
y2="367.26637" />
|
||||
<path
|
||||
d="m155.10919 269.94146h51.14959v39.91076c0 33.58458-51.84829 32.48924-51.84829 0z"
|
||||
fill="none"
|
||||
stroke="#b0bccf"
|
||||
stroke-width="8.39786"
|
||||
id="path2" />
|
||||
<g
|
||||
fill="#b0bccf"
|
||||
transform="translate(150.21155 265.74253)"
|
||||
id="g17">
|
||||
<g
|
||||
transform="matrix(2.000607 1.1550509 -1.1550509 2.000607 -114.54424 -361.12059)"
|
||||
id="g6">
|
||||
<rect
|
||||
height="6.677884"
|
||||
ry=".917437"
|
||||
width="1.109652"
|
||||
x="133.26457"
|
||||
y="113.53527"
|
||||
id="rect2" />
|
||||
<rect
|
||||
height="6.677884"
|
||||
ry=".917437"
|
||||
width="1.109652"
|
||||
x="145.20386"
|
||||
y="113.53527"
|
||||
id="rect3" />
|
||||
<rect
|
||||
height="14.584357"
|
||||
ry="1.550303"
|
||||
width="1.621357"
|
||||
x="135.81224"
|
||||
y="109.58203"
|
||||
id="rect4" />
|
||||
<rect
|
||||
height="14.584357"
|
||||
ry="1.550303"
|
||||
width="1.621357"
|
||||
x="142.14449"
|
||||
y="109.58203"
|
||||
id="rect5" />
|
||||
<rect
|
||||
height="20.411003"
|
||||
ry="2.16967"
|
||||
width="1.834875"
|
||||
x="138.87161"
|
||||
y="106.66871"
|
||||
id="rect6" />
|
||||
</g>
|
||||
<g
|
||||
stroke-width=".715877"
|
||||
id="g16">
|
||||
<g
|
||||
transform="matrix(1.3467588 0 0 1.3467588 -106.93441 30.218159)"
|
||||
id="g10">
|
||||
<path
|
||||
d="m149.74457.19113632q-2.17627 0-4.03756-.801776-1.83265-.83042902-3.20713-2.23354302-1.34584-1.431739-2.11899-3.37894-.74452-1.947173-.74452-4.2093413 0-2.290823.74452-4.180744.77315-1.889893 2.11899-3.207128 1.37448-1.345835 3.20713-2.061706 1.86129-.744523 4.03756-.744523h5.0684q2.26217 0 4.12346.744523 1.88989.744496 3.23576 2.090358 1.34583 1.345834 2.09036 3.235755.74449 1.861296.74449 4.123465 0 2.2621683-.77312 4.2093413-.74452 1.947201-2.11901 3.37894-1.34584 1.403114-3.20713 2.23354302-1.86127.801776-4.09481.801776zm5.0684-3.63665702q1.43176 0 2.63441-.515434 1.20268-.515432 2.06174-1.403113.88768-.916334 1.37448-2.176263.48681-1.259957.48681-2.7203223 0-1.460394-.48681-2.691697-.4868-1.259956-1.37448-2.147638-.85906-.887681-2.06174-1.374488-1.20265-.515432-2.63441-.515432h-5.0684q-1.40314 0-2.60579.515432-1.17406.486807-2.03311 1.374488-.85906.887682-1.34584 2.118984-.4868 1.231304-.4868 2.720351 0 1.4603653.4868 2.7203223.48678 1.259929 1.34584 2.176263.85905.887681 2.03311 1.403113 1.20265.515434 2.60579.515434z"
|
||||
id="path6" />
|
||||
<path
|
||||
d="m167.95643.19113632v-3.63665702h15.60612q1.46039 0 2.23351-.744497.80181-.744522.80181-1.975826 0-1.345861-.80181-2.004452-.77312-.687243-2.23351-.687243h-9.56411q-1.4604 0-2.66307-.429529-1.17405-.458153-2.00445-1.2313023-.80178-.801803-1.25996-1.889921-.42953-1.088145-.42953-2.376728 0-1.259929.4009-2.319421.42953-1.088145 1.23131-1.861295.83042-.773149 2.0331-1.202676 1.20265-.458154 2.74895-.458154h14.9475v3.636657h-14.9475q-1.25993 0-1.94717.687243-.68725.658618-.68725 1.861269 0 1.202675.68725 1.861294.71587.658591 1.91854.658591h9.53549q3.17847 0 4.81068 1.489047 1.66083 1.4890193 1.66083 4.5243373 0 1.317209-.40088 2.433981-.4009 1.11677-1.20267 1.947173-.80178.80180302-2.03311 1.25995502-1.20265.458154-2.83485.458154z"
|
||||
id="path7" />
|
||||
<path
|
||||
d="m193.35572.19113632v-20.81770132h3.63666v20.81770132z"
|
||||
id="path8" />
|
||||
<path
|
||||
d="m204.17973-15.042736v15.23387232h-3.46485v-18.89915432q0-1.002213.48678-1.574925.51544-.601338 1.37449-.601338.4009 0 .77315.171811.37225.171811.77315.572712l14.68985 14.5752533v-15.2338713h3.46487v19.0709653q0 1.03086602-.51544 1.60357902-.48678.572685-1.28859.572685-.88768 0-1.66084-.77315z"
|
||||
id="path9" />
|
||||
<path
|
||||
d="m232.47127.19113632v-17.18104432h-7.9033v-3.636657h19.44324v3.636657h-7.90327v17.18104432z"
|
||||
id="path10" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(1.3467588 0 0 1.3467588 -108.04477 30.218159)"
|
||||
id="g15">
|
||||
<path
|
||||
d="m140.46084 26.68477v-20.817701h15.23379q1.51767 0 2.74897.3436219 1.25993.343622 2.14764 1.030866.88768.6585907 1.37448 1.6894568.48678 1.0022394.48678 2.3194473 0 1.002214-.22917 1.746736-.22917.744497-.62999 1.288583-.40087.515433-.94496.887682-.51542.343621-1.08811.515432 1.43173.458153 2.34808 1.660831.91629 1.202676.91629 3.035319 0 1.431766-.48676 2.60579-.45817 1.145398-1.34588 1.975828-.85904.830401-2.11897 1.288581-1.23131.429527-2.74898.429527zm6.27104-9.048703v-3.063945h8.9914q1.54627 0 2.29079-.572711.74454-.572685.74454-1.947174 0-.830428-.31501-1.317208-.31499-.515432-.91633-.7731493-.57269-.2863435-1.40311-.3722476-.80178-.086037-1.80401-.086037h-10.22267v13.5730129h10.53765q.9736 0 1.80402-.143134.8304-.143133 1.43174-.458151.60135-.343624.91633-.859057.34363-.515433.34363-1.288581 0-1.288556-.80177-1.975801-.80181-.715897-2.34809-.715897z"
|
||||
id="path11" />
|
||||
<path
|
||||
d="m184.61602 5.867069h3.55074v11.62584q0 2.405326-.62994 4.209343-.62997 1.775389-1.97584 2.949412-1.34584 1.174052-3.49348 1.746737-2.11897.572712-5.15429.572712-3.0067 0-5.15433-.572712-2.11898-.572685-3.46485-1.746737-1.34583-1.174023-1.97582-2.949412-.62997-1.804017-.62997-4.209343v-11.62584h3.75119v11.62584q0 1.574925.37227 2.691697.40087 1.116772 1.25993 1.832641.88768.715871 2.34807 1.059492 1.4604.314996 3.60804.314996 2.14763 0 3.608-.314996 1.4604-.343621 2.31946-1.059492.88767-.715869 1.25994-1.832641.40088-1.116772.40088-2.691697z"
|
||||
id="path12" />
|
||||
<path
|
||||
d="m191.45981 26.68477v-20.817701h12.05537q2.20488 0 4.06618.7444961 1.86126.7445236 3.20713 2.0903583 1.34584 1.3458616 2.09036 3.2357826.77315 1.861267.77315 4.123437 0 2.262169-.77315 4.209369-.74452 1.947172-2.09036 3.37894-1.34587 1.403114-3.20713 2.233516-1.8613.801802-4.06618.801802zm12.05537-3.636657q1.40312 0 2.57716-.515433 1.20267-.515433 2.06173-1.403113.88768-.916334 1.37446-2.176263.48681-1.259957.48681-2.720351 0-1.460393-.48681-2.691697-.48678-1.259929-1.37446-2.147636-.85906-.887681-2.06173-1.374461-1.17404-.5154326-2.57716-.5154326h-8.44734v13.5443866z"
|
||||
id="path13" />
|
||||
<path
|
||||
d="m216.63004 26.68477v-20.817701h12.05537q2.20489 0 4.06618.7444961 1.86128.7445236 3.20713 2.0903583 1.34582 1.3458616 2.09036 3.2357826.77314 1.861267.77314 4.123437 0 2.262169-.77314 4.209369-.74454 1.947172-2.09036 3.37894-1.34585 1.403114-3.20713 2.233516-1.86129.801802-4.06618.801802zm12.05537-3.636657q1.4031 0 2.57712-.515433 1.2027-.515433 2.06175-1.403113.88767-.916334 1.37448-2.176263.48681-1.259957.48681-2.720351 0-1.460393-.48681-2.691697-.48681-1.259929-1.37448-2.147636-.85905-.887681-2.06175-1.374461-1.17402-.5154326-2.57712-.5154326h-8.44737v13.5443866z"
|
||||
id="path14" />
|
||||
<path
|
||||
d="m248.41493 26.68477v-7.903279l-11.05315-12.914422h4.89661l8.10371 9.592735 8.13238-9.592735h4.63887l-11.08179 12.914422v7.903279z"
|
||||
id="path15" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
d="m216.25711 396.16259c1.19733.32192 1.97921 1.53061 1.76391 2.72691-.0967.53749-.31684.94222-.73572 1.35272l-.36157.35439.60185 2.36734c.33104 1.30207.60189 2.38306.60189 2.40228 0 .0185.12077.0352.26843.0358 1.09307.005 2.31469.79757 2.8096 1.82649.35373.73545.44281 1.40894.28541 2.15841-.23476 1.11741-1.09438 2.0875-2.18548 2.46594-.74754.25916-1.80496.19151-2.50205-.16061l-.27799-.14024-.76785.80922c-.42237.44506-1.66381 1.74812-2.75879 2.89567l-1.99092 2.08648.0877.16959c.1529.29559.22734.80689.18379 1.26276-.10533 1.10229-.91509 1.9393-2.05982 2.129-.67836.11247-1.42364-.12265-1.94988-.61482-1.43298-1.3404-.80486-3.67401 1.10972-4.12289.43849-.10292.81212-.0845 1.27881.0628l.30487.0961.67993-.69922c.73073-.75151 3.74385-3.91076 4.41561-4.62975l.40828-.43701-.19583-.38861c-.1183-.23474-.23351-.58734-.29128-.8906-.16185-.8494.0247-1.69565.53722-2.45199.24834-.36555.64624-.74515.98441-.93885.11644-.0667.2122-.14671.21282-.17761.00092-.0311-.26935-1.12825-.60047-2.43816l-.60186-2.38161-.28294-.0333c-.96418-.11429-1.81011-.98551-1.9728-2.03204-.2607-1.67638 1.35142-3.10758 3.0007-2.66415zm-1.23618 1.1525c-.42796.2184-.69165.66813-.6961 1.18726-.009 1.1681 1.41302 1.7423 2.25596.91029.69023-.68131.41286-1.8382-.52408-2.18585-.27398-.10154-.74144-.0617-1.03578.0883zm2.49362 9.67176c-.39528.17483-.70527.46369-.91726.85472-.1807.33321-.19398.39342-.19306.86691.001.60302.12881.92389.52351 1.31892.36092.36114.78416.53611 1.31095.54197.3745.005.47454-.0185.80535-.181.45661-.22455.78479-.57647.95269-1.0214.16742-.44376.13468-1.05572-.0803-1.49514-.42629-.87126-1.52094-1.27461-2.40181-.88493zm-8.98863 10.26085c-.58756.17452-.93798.64869-.93829 1.26964-.00019.39706.0939.64162.3558.92386.6093.65689 1.72076.49351 2.12212-.31192.39833-.79936-.0571-1.74029-.92721-1.91585-.2956-.0596-.29622-.0596-.61242.0342zm-.91658-16.19663c.54336.11091 1.04535.59268 1.19529 1.14691.0945.34981.0321.82901-.15228 1.16802-.28449.52263-.77487.8081-1.3796.80306-.61906-.006-1.09481-.28385-1.38803-.81317-.14548-.26285-.1668-.35817-.1668-.75325 0-.39672.0216-.48994.16958-.75888.33861-.61206 1.03551-.93293 1.72178-.79269zm19.26734 1.91417c.40507.18968.62428.39557.80352.75428.61165 1.224-.51975 2.60369-1.8242 2.22448-1.12115-.32594-1.53008-1.67829-.7689-2.543.32544-.36975.60637-.50142 1.12492-.5273.33687-.0154.47346.003.66466.0917zm-15.95747.93349c.90716.52398 1.05221.71182.85069 1.10151-.0985.19088-.36855.31468-.57368.26317-.20912-.0526-1.44332-.75804-1.5632-.89364-.14301-.16184-.1288-.51107.0278-.69175.24865-.28633.43756-.25327 1.2579.22056zm12.92266 1.55461c.21035.21035.22703.51401.038.70032-.22672.22363-1.48218.887-1.67878.887-.39015 0-.65144-.51066-.42598-.83254.12973-.18533 1.50426-.92004 1.72104-.92004.12416 0 .23196.0516.34567.16526zm-9.81811.22239c.85277.48906.93771.56403.93771.82743 0 .11333-.0278.25731-.0611.32001-.0883.16496-.39802.28078-.59651.22302-.17823-.0519-1.41738-.75874-1.53747-.87698-.0414-.0404-.089-.17792-.10595-.30487-.0278-.20326-.009-.25205.14115-.40353.26534-.26562.43652-.23537 1.22207.21528zm6.74571 6.69353c.17328.1875.80665 1.37499.80665 1.5127 0 .33734-.33981.61109-.66883.53884-.21931-.0482-.27552-.11831-.65851-.82191-.39978-.73452-.43725-.92751-.22826-1.17585.12726-.15104.19181-.181.39182-.181.17575 0 .27089.034.35716.12726zm1.40847 2.78008c.14363.0636.24927.21344.55121.78257.41535.78288.45479.98288.23691 1.20074-.18409.1841-.40105.23135-.60952.1325-.23074-.10932-.91219-1.33277-.9164-1.64515-.006-.3818.38135-.62839.73777-.47072zm2.18267 2.48771c1.15377.41173 1.42086 1.91182.48327 2.71437-.62249.53282-1.5209.48566-2.11776-.11114-.6359-.63589-.63367-1.56802.006-2.20697.4396-.43963 1.07473-.59407 1.62917-.39621z"
|
||||
fill="#b0bccf"
|
||||
stroke-width=".213676"
|
||||
id="path17" />
|
||||
<path
|
||||
d="m137.05192 400.96156h31.22083v31.22083h-31.22083z"
|
||||
fill="none"
|
||||
stroke="#b0bccf"
|
||||
stroke-width="2.427"
|
||||
id="path18" />
|
||||
<text
|
||||
style="font-size:25.4;font-family:Good Times;letter-spacing:-.134937;word-spacing:-.690562;stroke:#b0bccf;stroke-width:1.3;fill:url(#b)"
|
||||
x="229.26233"
|
||||
y="418.78128"
|
||||
id="text21"><tspan
|
||||
fill="url(#b)"
|
||||
stroke-width="1.3"
|
||||
x="229.26233"
|
||||
y="418.78128"
|
||||
id="tspan21"><tspan
|
||||
fill="url(#b)"
|
||||
stroke-width="1.3"
|
||||
id="tspan19">i<tspan
|
||||
fill="url(#b)"
|
||||
stroke-width="1.3"
|
||||
id="tspan18">c</tspan></tspan><tspan
|
||||
fill="url(#b)"
|
||||
stroke="none"
|
||||
stroke-width="1.3"
|
||||
id="tspan20">GRAPH</tspan></tspan></text>
|
||||
<path
|
||||
d="m233.84345 353.88056c1.19732.32192 1.9792 1.53061 1.76391 2.72691-.0967.53749-.31685.94222-.73573 1.35272l-.36158.35439.60186 2.36734c.33103 1.30207.60189 2.38306.60189 2.40228 0 .0185.12077.0352.26842.0358 1.09307.005 2.31471.79757 2.8096 1.82649.35373.73545.44282 1.40893.28541 2.1584-.23475 1.11741-1.09437 2.0875-2.18547 2.46594-.74753.25916-1.80496.19151-2.50205-.16061l-.278-.14024-.76785.80922c-.42237.44506-1.6638 1.74812-2.75879 2.89567l-1.99091 2.08648.0877.16959c.1529.29559.22734.80689.18379 1.26276-.10534 1.10229-.9151 1.9393-2.05982 2.129-.67837.11247-1.42364-.12265-1.94989-.61482-1.43297-1.3404-.80485-3.67401 1.10973-4.12289.43849-.10292.81211-.0845 1.27881.0628l.30486.0961.67995-.69922c.73072-.75151 3.74385-3.91076 4.41561-4.62975l.40828-.43701-.19583-.38861c-.11831-.23474-.23352-.58734-.29128-.8906-.16185-.8494.0247-1.69564.53721-2.45198.24834-.36555.64625-.74515.98441-.93885.11645-.0667.2122-.14671.21282-.17761.00092-.0311-.26934-1.12825-.60047-2.43816l-.60186-2.38161-.28293-.0333c-.96418-.11429-1.81012-.98551-1.97281-2.03204-.2607-1.67638 1.35143-3.10758 3.00071-2.66415zm-1.23618 1.1525c-.42796.2184-.69165.66813-.6961 1.18726-.009 1.1681 1.41301 1.7423 2.25596.91029.69023-.68131.41285-1.8382-.52408-2.18585-.27398-.10154-.74145-.0617-1.03578.0883zm2.49361 9.67176c-.39528.17483-.70527.46369-.91726.85472-.1807.3332-.19398.39341-.19305.8669.001.60302.1288.92389.5235 1.31892.36093.36114.78416.53611 1.31096.54197.37449.005.47454-.0185.80535-.181.45659-.22455.78478-.57647.95269-1.0214.16741-.44376.13467-1.05572-.0803-1.49513-.42629-.87126-1.52094-1.27461-2.40181-.88493zm-8.98863 10.26084c-.58755.17452-.93798.64869-.93829 1.26964-.00018.39706.0939.64162.3558.92386.60931.65689 1.72076.49351 2.12212-.31192.39834-.79936-.0571-1.74029-.9272-1.91585-.2956-.0596-.29622-.0596-.61243.0342zm-.91658-16.19662c.54336.11091 1.04536.59268 1.19529 1.14691.0945.34981.0321.82901-.15228 1.16802-.28448.52263-.77486.8081-1.3796.80306-.61906-.006-1.0948-.28385-1.38802-.81317-.14549-.26285-.1668-.35817-.1668-.75325 0-.39672.0216-.48994.16958-.75888.33859-.61206 1.03549-.93293 1.72177-.79269zm19.26734 1.91417c.40507.18968.62428.39557.80353.75428.61165 1.224-.51976 2.60369-1.8242 2.22448-1.12116-.32594-1.53009-1.67829-.76891-2.543.32544-.36975.60637-.50142 1.12493-.5273.33686-.0154.47345.003.66465.0917zm-15.95746.93349c.90715.52398 1.05221.71182.85069 1.10151-.0985.19088-.36856.31468-.57369.26317-.20911-.0526-1.44332-.75804-1.56319-.89364-.14302-.16184-.12881-.51107.0278-.69175.24865-.28633.43756-.25327 1.25789.22056zm12.92265 1.55461c.21035.21035.22703.51401.038.70032-.22672.22363-1.48217.887-1.67878.887-.39015 0-.65143-.51066-.42598-.83254.12973-.18533 1.50426-.92004 1.72104-.92004.12417 0 .23197.0516.34567.16526zm-9.8181.22239c.85276.48906.93771.56403.93771.82743 0 .11333-.0278.25731-.0611.32001-.0883.16496-.39803.28078-.59652.22302-.17822-.0519-1.41737-.75874-1.53746-.87698-.0414-.0404-.0889-.17792-.10596-.30487-.0278-.20326-.009-.25205.14116-.40353.26533-.26562.43651-.23537 1.22207.21528zm6.7457 6.69352c.17329.1875.80665 1.37499.80665 1.5127 0 .33734-.3398.61109-.66883.53884-.2193-.0482-.27552-.11831-.6585-.82191-.39979-.73452-.43726-.92751-.22827-1.17585.12726-.15104.19182-.181.39182-.181.17575 0 .27089.034.35716.12726zm1.40848 2.78008c.14363.0636.24927.21344.5512.78257.41536.78288.4548.98288.23691 1.20074-.18409.1841-.40105.23135-.60952.1325-.23073-.10932-.91219-1.33277-.91639-1.64515-.006-.3818.38135-.62839.73777-.47072zm2.18266 2.48771c1.15377.41173 1.42086 1.91182.48328 2.71437-.62249.53282-1.52091.48566-2.11777-.11114-.63589-.63589-.63367-1.56802.006-2.20697.4396-.43963 1.07473-.59407 1.62917-.39621z"
|
||||
fill="#b0bccf"
|
||||
stroke-width=".213676"
|
||||
id="path21" />
|
||||
<text
|
||||
style="font-size:25.4;font-family:Good Times;letter-spacing:-.134937;word-spacing:-.690562;stroke:#b0bccf;stroke-width:1.3;fill:url(#c)"
|
||||
x="246.84866"
|
||||
y="376.49924"
|
||||
id="text25"><tspan
|
||||
fill="url(#c)"
|
||||
stroke-width="1.3"
|
||||
x="246.84866"
|
||||
y="376.49924"
|
||||
id="tspan25"><tspan
|
||||
fill="url(#c)"
|
||||
stroke-width="1.3"
|
||||
id="tspan23">i<tspan
|
||||
fill="url(#c)"
|
||||
stroke-width="1.3"
|
||||
id="tspan22">c</tspan></tspan><tspan
|
||||
fill="url(#c)"
|
||||
stroke="none"
|
||||
stroke-width="1.3"
|
||||
id="tspan24">GRAPH</tspan></tspan></text>
|
||||
<path
|
||||
d="m160.08378-106.54659c-1.65246 0-3.18532-.3044-4.59861-.91319-1.39153-.63054-2.60913-1.47851-3.65277-2.54391-1.02193-1.08714-1.8264-2.36996-2.41346-3.84847-.56531-1.47851-.84797-3.0766-.84797-4.79428 0-1.73943.28266-3.32665.84797-4.76167.58706-1.43502 1.39153-2.65262 2.41346-3.65279 1.04364-1.02191 2.26124-1.80465 3.65277-2.34822 1.41329-.56531 2.94615-.84797 4.59861-.84797h12.84998v4.142h-12.84998c-1.06539 0-2.0547.19569-2.96789.58706-.89146.36963-1.66333.89145-2.31561 1.56548-.65227.67402-1.16323 1.47851-1.53287 2.41345-.36962.93494-.55443 1.96772-.55443 3.09835 0 1.10888.18481 2.14166.55443 3.09834.36964.95668.8806 1.78291 1.53287 2.47868.65228.67403 1.42415 1.20673 2.31561 1.59809.91319.39138 1.9025.58706 2.96789.58706h10.143v-6.16408h-10.69745v-3.48971h14.87206v13.79579zm39.97478 0-6.001-6.58806h-8.87106v-3.65279h9.8821c1.50025 0 2.63087-.40224 3.39187-1.20671.761-.82624 1.14149-2.01122 1.14149-3.55495 0-1.54374-.40223-2.68524-1.2067-3.42449-.78274-.761-1.89162-1.1415-3.32665-1.1415h-12.91521v19.5685h-4.10939v-23.7105h17.0246c1.34805 0 2.55476.20656 3.62018.61966 1.06539.39137 1.96772.96756 2.70697 1.72856.73925.73925 1.30457 1.64158 1.69592 2.70697.39137 1.06541.58707 2.26126.58707 3.58756 0 1.9786-.43486 3.64192-1.30457 4.98998-.84798 1.3263-2.04382 2.28298-3.58755 2.87004l7.1425 7.20773zm32.57136 0-3.55494-5.9684h-15.54795l1.95684-3.35925h11.60163l-5.28349-8.87106-10.76267 18.19871h-4.72905l13.63271-22.69947c.23918-.41311.52184-.73925.84798-.97842s.71751-.35876 1.1741-.35876c.45661 0 .8371.11959 1.14151.35876.32614.23917.60879.56531.84796.97842l13.66533 22.69947zm14.50312-6.58806v-4.01154h9.52333c1.45677 0 2.5874-.41312 3.39188-1.23934.82622-.82622 1.23934-1.91336 1.23934-3.26141 0-1.3698-.41311-2.45694-1.23934-3.26142-.80448-.80449-1.93511-1.20673-3.39188-1.20673h-12.55646v19.56851h-4.10939v-23.71051h16.66585c1.3698 0 2.5874.20656 3.6528.61967 1.08713.39137 2.00033.95669 2.73957 1.69594.73927.71751 1.30458 1.5981 1.69595 2.64175s.58706 2.21776.58706 3.52233c0 1.28283-.19569 2.45693-.58706 3.52233-.39137 1.04365-.95668 1.94598-1.69595 2.70698-.73924.76099-1.65244 1.35892-2.73957 1.79377-1.0654.41312-2.283.61967-3.6528.61967zm40.04003 6.6859v-10.30607h-16.58472v-3.6854h16.58472v-9.81687h4.142v23.80835zm-19.07929 0v-23.80834h4.142v23.80834z"
|
||||
fill="#b0bccf"
|
||||
stroke-width=".571284"
|
||||
id="path25" />
|
||||
<g
|
||||
fill="#b0bccf"
|
||||
transform="translate(-4.697965 -3.47097)"
|
||||
id="g30">
|
||||
<path
|
||||
d="m34.289873 3.6988367c3.361842.9038753 5.557206 4.2976421 4.952696 7.6566013-.271451 1.509171-.889649 2.645567-2.065774 3.798171l-1.015231.995051 1.689893 6.647017c.92947 3.655948 1.689986 6.691143 1.689986 6.745109 0 .05191.339083.09879.75367.100544 3.069134.01417 6.499228 2.239415 7.888795 5.128425.993211 2.064987 1.243333 3.956016.801379 6.060368-.659145 3.137467-3.0728 5.861284-6.136378 6.923862-2.098914.727665-5.067966.537721-7.025256-.450956l-.780543-.393762-2.155975 2.272123c-1.185921 1.249639-4.671613 4.908362-7.746124 8.130455l-5.590066 5.858412.2463.476172c.429297.829965.638321 2.265586.516031 3.545575-.295768 3.09501-2.569405 5.44516-5.783562 5.977797-1.904716.315796-3.997291-.344367-5.4748908-1.726292-4.0234831-3.763564-2.259866-10.315868 3.1159048-11.576229 1.231192-.288984 2.280245-.237267 3.590625.176327l.856002.269833 1.909158-1.963278c2.051721-2.110081 10.511966-10.980623 12.398129-12.999392l1.146371-1.22704-.549864-1.091124c-.332168-.659121-.655653-1.649145-.817846-2.500637-.454457-2.384943.06938-4.76104 1.508386-6.884683.697291-1.0264 1.814514-2.092234 2.764037-2.636108.326953-.187285.595803-.41193.597548-.498687.0023-.0873-.756272-3.167902-1.686001-6.845858l-1.689893-6.687086-.794443-.09351c-2.707209-.320913-5.082427-2.767113-5.539228-5.705565-.731992-4.7069341 3.794526-8.7254482 8.425377-7.4804036zm-3.470949 3.2359784c-1.201612.6132164-1.942018 1.8759804-1.9545 3.3335879-.02613 3.279784 3.967455 4.892026 6.334271 2.555905 1.938034-1.91298 1.159224-5.1612872-1.471507-6.1374239-.769292-.2851037-2.081828-.1732435-2.908264.247931zm7.001571 27.1563489c-1.109865.490881-1.980271 1.301951-2.575475 2.399875-.507369.935587-.544648 1.104648-.542058 2.434105.0023 1.693162.361653 2.594109 1.469876 3.70327 1.013415 1.014001 2.201761 1.50529 3.68091 1.521733 1.051489.01418 1.332411-.05191 2.261271-.508202 1.282007-.630491 2.203497-1.618617 2.674946-2.867892.47007-1.24598.378132-2.964245-.225524-4.198051-1.196924-2.446319-4.270487-3.578846-6.743794-2.484698zm-25.238248 28.81039c-1.649744.490015-2.6336728 1.821394-2.6345398 3.564889-.000516 1.114856.2636818 1.801541.9990238 2.594015 1.7108 1.84441 4.831532 1.385673 5.958482-.875808 1.118442-2.244443-.160423-4.88639-2.603401-5.379321-.83-.16735-.831723-.16735-1.719565.09597zm-2.573577-45.476866c1.525647.311415 2.935148 1.664123 3.356123 3.2203.265428.982195.09023 2.327685-.427561 3.279562-.798767 1.467439-2.175664 2.268971-3.8736418 2.254827-1.738199-.01688-3.0739845-.796997-3.8972918-2.283211-.408484-.738025-.4683334-1.005668-.4683334-2.114982 0-1.113907.0607-1.375654.4761389-2.130778.9507168-1.718534 2.9074673-2.619481 4.8344011-2.225718zm54.098787 5.374599c1.137345.532589 1.752859 1.110685 2.256138 2.117865 1.717397 3.436752-1.459377 7.310651-5.121991 6.245895-3.147967-.915173-4.296166-4.712301-2.158918-7.140231.913757-1.038179 1.702562-1.407884 3.158551-1.480542.945855-.04324 1.329376.0082 1.86622.25747zm-44.805333 2.621064c2.547126 1.471223 2.954402 1.998645 2.388577 3.092817-.276679.535951-1.034839.883554-1.610812.738928-.587142-.147709-4.052548-2.128424-4.389133-2.509167-.401546-.454412-.361654-1.434978.07804-1.94229.698157-.803958 1.228589-.711129 3.531924.619289zm36.284207 4.365026c.590612.590622.637442 1.443226.106641 1.966347-.636585.627913-4.161652 2.490534-4.713684 2.490534-1.09546 0-1.829091-1.43384-1.196069-2.33761.364266-.520367 4.22367-2.583292 4.832329-2.583292.348633 0 .651316.144883.97057.464021zm-27.567256.62442c2.394402 1.373182 2.632898 1.583682 2.632898 2.323266 0 .3182-.07804.722464-.171696.89851-.248048.463188-1.117575.788383-1.674893.626202-.500432-.145717-3.979701-2.130381-4.316896-2.462385-.116252-.113436-.249747-.499565-.297492-.856003-.07804-.570724-.02601-.707709.396343-1.133033.745-.745809 1.225638-.66088 3.431313.604451zm18.940566 18.794077c.486545.526472 2.264916 3.860693 2.264916 4.247367 0 .947178-.954093 1.715804-1.877924 1.512956-.615774-.135345-.773619-.332191-1.848957-2.307774-1.122521-2.062373-1.227726-2.604257-.640922-3.301536.357316-.424093.538578-.508212 1.100136-.508212.493495 0 .76061.0954 1.002845.357317zm3.954727 7.805904c.40328.178576.699892.599307 1.54767 2.197309 1.16622 2.198164 1.276978 2.759724.665203 3.371428-.516911.516922-1.126084.649582-1.71141.372026-.647858-.306936-2.56126-3.742142-2.573049-4.619239-.01758-1.072023 1.070746-1.764391 2.071505-1.321689zm6.12847 6.984987c3.239552 1.156062 3.989484 5.368012 1.356949 7.621408-1.747831 1.496047-4.270417 1.363632-5.946259-.312057-1.785474-1.785451-1.779227-4.402692.01758-6.196733 1.234322-1.234391 3.017615-1.668024 4.574379-1.112466z"
|
||||
stroke-width=".599959"
|
||||
id="path26" />
|
||||
<g
|
||||
transform="translate(0 -4.284847)"
|
||||
id="g29">
|
||||
<g
|
||||
transform="matrix(1.8224234 0 0 1.8224234 -68.586524 -59.923346)"
|
||||
id="g28">
|
||||
<path
|
||||
d="m108.86906 44.633785c-1.92434 0-3.7096.329148-5.35541.987459-1.62051.633017-3.0382 1.544208-4.253571 2.734257-1.19005 1.164725-2.126862 2.583067-2.810509 4.254205-.658325 1.671128-.987458 3.519147-.987458 5.544763 0 2.000305.329133 3.861107.987458 5.58289.683647 1.721765 1.620459 3.215674 2.810509 4.48169 1.215371 1.240693 2.633061 2.228732 4.253571 2.963011 1.64581.708975 3.43135 1.096336 5.35541 1.063075l5.6693-.09786v-4.823551l-5.6693.09786c-1.2407 0-2.39266-.227954-3.45611-.683722-1.03813-.455759-1.93716-1.075626-2.69677-1.86054-.75961-.810259-1.35447-1.77267-1.78492-2.886761-.43044-1.114098-.64559-2.316635-.64559-3.607971 0-1.316652.21515-2.519843.64559-3.608609.43045-1.088777 1.02531-2.025574 1.78492-2.810509.75961-.784923 1.65864-1.392603 2.69677-1.823048 1.06345-.455769 2.21559-.704533 3.45611-.683088l5.6693.09786v-4.823554zm10.72224.185546v4.823551l4.24214.07307v-4.823551zm4.24214 22.343633-4.24214.07308v4.823551l4.24214-.07308z"
|
||||
fill="#b0bccf"
|
||||
stroke="#b0bccf"
|
||||
stroke-width="1.22963"
|
||||
id="path27" />
|
||||
<path
|
||||
d="m83.39564 72.86192v-29.234034h5.106903v29.234034z"
|
||||
stroke-width=".692748"
|
||||
id="path28" />
|
||||
</g>
|
||||
<path
|
||||
d="m192.22345 72.48702c-3.58109 0-6.90299-.659674-9.96577-1.979-3.01563-1.366462-5.65432-3.204121-7.91602-5.512979-2.21466-2.355972-3.95805-5.136007-5.23028-8.340127-1.2251-3.204122-1.83766-6.667387-1.83766-10.38982 0-3.769567.61256-7.209276 1.83766-10.319148 1.27223-3.109873 3.01562-5.748567 5.23028-7.916064 2.2617-2.21461 4.90039-3.910907 7.91602-5.088893 3.06278-1.2251 6.38468-1.83766 9.96577-1.83766h27.84756v8.976245h-27.84756c-2.30883 0-4.4528.424085-6.4318 1.272234-1.9319.801035-3.60465 1.931885-5.01822 3.392596-1.41354 1.460688-2.52087 3.20412-3.32192 5.230254-.80101 2.026135-1.20152 4.264301-1.20152 6.714522 0 2.403086.40051 4.641252 1.20152 6.7145.80105 2.073247 1.90838 3.863793 3.32192 5.371617 1.41357 1.460709 3.08632 2.615138 5.01822 3.463266 1.979.84817 4.12297 1.272233 6.4318 1.272233h21.98119v-13.358351h-23.18275v-7.562648h32.22966v29.897244z"
|
||||
stroke-width="1.23805"
|
||||
id="path29" />
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
d="m48.054683 215.40426h51.14959v39.91076c0 33.58458-51.84829 32.48924-51.84829 0z"
|
||||
fill="none"
|
||||
stroke="#b0bccf"
|
||||
stroke-width="8.39786"
|
||||
id="path30" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 27 KiB |
BIN
frontend/src/assets/images/image-11ldby0i.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
frontend/src/assets/images/image-1hmfchzd.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
frontend/src/assets/images/image-4z9yhycr.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 28 KiB |
1
frontend/src/assets/images/logo-ext.svg
Normal file
|
After Width: | Height: | Size: 33 KiB |
1
frontend/src/assets/images/logo-full.svg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
frontend/src/assets/images/logo-ghorg.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
frontend/src/assets/images/logo-mark-primary.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
1
frontend/src/assets/images/logo-mark-primary.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="69.776581mm" viewBox="0 0 62.144792 69.776582" width="62.144794mm" xmlns="http://www.w3.org/2000/svg"><path d="m29.591908.22786659c3.361842.90387531 5.557206 4.29764211 4.952696 7.65660131-.271451 1.509171-.889649 2.6455671-2.065774 3.7981711l-1.015231.995051 1.689893 6.647017c.92947 3.655948 1.689986 6.691143 1.689986 6.745109 0 .05191.339083.09879.75367.100544 3.069134.01417 6.499228 2.239415 7.888795 5.128425.993211 2.064987 1.243333 3.956016.801379 6.060368-.659145 3.137467-3.0728 5.861284-6.136378 6.923862-2.098914.727665-5.067966.537721-7.025256-.450956l-.780543-.393762-2.155975 2.272123c-1.185921 1.249639-4.671613 4.908362-7.746124 8.130455l-5.590066 5.858412.2463.476172c.429297.829965.638321 2.265586.516031 3.545575-.295768 3.09501-2.569405 5.44516-5.783562 5.977797-1.904716.315796-3.997291-.344367-5.4748908-1.726292-4.0234831-3.763564-2.259866-10.315868 3.1159048-11.576229 1.231192-.288984 2.280245-.237267 3.590625.176327l.856002.269833 1.909158-1.963278c2.051721-2.110081 10.511966-10.980623 12.398129-12.999392l1.146371-1.22704-.549864-1.091124c-.332168-.659121-.655653-1.649145-.817846-2.500637-.454457-2.384943.06938-4.76104 1.508386-6.884683.697291-1.0264 1.814514-2.092234 2.764037-2.636108.326953-.187285.595803-.41193.597548-.498687.0023-.0873-.756272-3.167902-1.686001-6.845858l-1.689893-6.687086-.794443-.09351c-2.707209-.320913-5.082427-2.767113-5.539228-5.7055651-.731992-4.7069341 3.794526-8.7254482 8.425377-7.48040361zm-3.470949 3.23597841c-1.201612.6132164-1.942018 1.8759804-1.9545 3.3335879-.02613 3.2797841 3.967455 4.8920261 6.334271 2.555905 1.938034-1.91298 1.159224-5.1612872-1.471507-6.1374239-.769292-.2851037-2.081828-.1732435-2.908264.247931zm7.001571 27.156349c-1.109865.490881-1.980271 1.301951-2.575475 2.399875-.507369.935587-.544648 1.104648-.542058 2.434105.0023 1.693162.361653 2.594109 1.469876 3.70327 1.013415 1.014001 2.201761 1.50529 3.68091 1.521733 1.051489.01418 1.332411-.05191 2.261271-.508202 1.282007-.630491 2.203497-1.618617 2.674946-2.867892.47007-1.24598.378132-2.964245-.225524-4.198051-1.196924-2.446319-4.270487-3.578846-6.743794-2.484698zm-25.238248 28.81039c-1.649744.490015-2.6336728 1.821394-2.6345398 3.564889-.000516 1.114856.2636818 1.801541.9990238 2.594015 1.7108 1.84441 4.831532 1.385673 5.958482-.875808 1.118442-2.244443-.160423-4.88639-2.603401-5.379321-.83-.16735-.831723-.16735-1.719565.09597zm-2.573577-45.476866c1.525647.311415 2.935148 1.664123 3.356123 3.2203.265428.982195.09023 2.327685-.427561 3.279562-.798767 1.467439-2.175664 2.268971-3.8736418 2.254827-1.738199-.01688-3.0739845-.796997-3.8972918-2.283211-.408484-.738025-.4683334-1.005668-.4683334-2.114982 0-1.113907.0607-1.375654.4761389-2.130778.9507168-1.718534 2.9074673-2.619481 4.8344011-2.225718zm54.098787 5.374599c1.137345.532589 1.752859 1.110685 2.256138 2.117865 1.717397 3.436752-1.459377 7.310651-5.121991 6.245895-3.147967-.915173-4.296166-4.712301-2.158918-7.140231.913757-1.038179 1.702562-1.407884 3.158551-1.480542.945855-.04324 1.329376.0082 1.86622.25747zm-44.805333 2.621064c2.547126 1.471223 2.954402 1.998645 2.388577 3.092817-.276679.535951-1.034839.883554-1.610812.738928-.587142-.147709-4.052548-2.128424-4.389133-2.509167-.401546-.454412-.361654-1.434978.07804-1.94229.698157-.803958 1.228589-.711129 3.531924.619289zm36.284207 4.365026c.590612.590622.637442 1.443226.106641 1.966347-.636585.627913-4.161652 2.490534-4.713684 2.490534-1.09546 0-1.829091-1.43384-1.196069-2.33761.364266-.520367 4.22367-2.583292 4.832329-2.583292.348633 0 .651316.144883.97057.464021zm-27.567256.62442c2.394402 1.373182 2.632898 1.583682 2.632898 2.323266 0 .3182-.07804.722464-.171696.89851-.248048.463188-1.117575.788383-1.674893.626202-.500432-.145717-3.979701-2.130381-4.316896-2.462385-.116252-.113436-.249747-.499565-.297492-.856003-.07804-.570724-.02601-.707709.396343-1.133033.745-.745809 1.225638-.66088 3.431313.604451zm18.940566 18.794077c.486545.526472 2.264916 3.860693 2.264916 4.247367 0 .947178-.954093 1.715804-1.877924 1.512956-.615774-.135345-.773619-.332191-1.848957-2.307774-1.122521-2.062373-1.227726-2.604257-.640922-3.301536.357316-.424093.538578-.508212 1.100136-.508212.493495 0 .76061.0954 1.002845.357317zm3.954727 7.805904c.40328.178576.699892.599307 1.54767 2.197309 1.16622 2.198164 1.276978 2.759724.665203 3.371428-.516911.516922-1.126084.649582-1.71141.372026-.647858-.306936-2.56126-3.742142-2.573049-4.619239-.01758-1.072023 1.070746-1.764391 2.071505-1.321689zm6.12847 6.984987c3.239552 1.156062 3.989484 5.368012 1.356949 7.621408-1.747831 1.496047-4.270417 1.363632-5.946259-.312057-1.785474-1.785451-1.779227-4.402692.01758-6.196733 1.234322-1.234391 3.017615-1.668024 4.574379-1.112466z" fill="#2b3bff" stroke-width=".599959"/></svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
1
frontend/src/assets/images/logo-mark.svg
Normal file
|
After Width: | Height: | Size: 22 KiB |
1
frontend/src/assets/images/logo-primary.svg
Normal file
|
After Width: | Height: | Size: 23 KiB |
@@ -1 +0,0 @@
|
||||
<svg height="12.082338mm" viewBox="0 0 42.157818 12.082338" width="42.157818mm" xmlns="http://www.w3.org/2000/svg"><g fill="#f9f9f9" stroke-width=".264583" transform="translate(-81.813094 -121.379916)"><path d="m97.375169 131.26811q-.804331 0-1.492245-.29633-.677331-.30692-1.18533-.8255-.497415-.52917-.783164-1.24883-.275166-.71967-.275166-1.55575 0-.84666.275166-1.54516.285749-.6985.783164-1.18533.507999-.49741 1.18533-.762.687914-.27516 1.492245-.27516h1.873244q.836077 0 1.523997.27516.6985.27517 1.19592.77259.49741.49741.77257 1.19591.27517.68791.27517 1.52399 0 .83608-.28575 1.55575-.27516.71966-.78316 1.24883-.49741.51858-1.18533.8255-.68792.29633-1.513417.29633zm1.873244-1.34408q.529165 0 .973667-.1905.4445-.1905.762-.51858.32808-.33867.508-.80433.17991-.46567.17991-1.00542 0-.53974-.17991-.99483-.17992-.46566-.508-.79374-.3175-.32809-.762-.508-.444502-.1905-.973667-.1905h-1.873244q-.518581 0-.96308.1905-.433915.17991-.751414.508-.317499.32808-.497415.78316-.179917.45508-.179917 1.00541 0 .53975.179917 1.00542.179916.46566.497415.80433.317499.32808.751414.51858.444499.1905.96308.1905z"/><path d="m104.10613 131.26811v-1.34408h5.7679q.53975 0 .8255-.27517.29633-.27516.29633-.73024 0-.49742-.29633-.74084-.28575-.25399-.8255-.25399h-3.53482q-.53975 0-.98425-.15875-.43391-.16934-.74083-.45509-.29633-.29633-.46566-.69849-.15875-.40217-.15875-.87842 0-.46566.14816-.85724.15875-.40217.45508-.68792.30692-.28575.75142-.4445.4445-.16933 1.016-.16933h5.52448v1.34408h-5.52448q-.46567 0-.71967.254-.254.24341-.254.68791 0 .4445.254.68792.26458.24341.70908.24341h3.52424q1.17475 0 1.778.55033.61383.55034.61383 1.67217 0 .48683-.14817.89958-.14817.41274-.4445.71966-.29633.29633-.75141.46567-.4445.16933-1.04775.16933z"/><path d="m113.49351 131.26811v-7.69406h1.34408v7.69406z"/><path d="m115.7054 131.26811v-7.69406h5.63031q.56092 0 1.016.127.46567.127.79375.381.32808.24341.508.62441.17991.37042.17991.85725 0 .37041-.0847.64558-.0847.27517-.23284.47625-.14816.1905-.34925.32808-.1905.127-.40216.1905.52916.16933.86783.61383.33866.4445.33866 1.12183 0 .52917-.17991.96308-.16933.42333-.49742.73025-.3175.30692-.78316.47625-.45508.15875-1.016.15875zm2.31774-3.34432v-1.13242h3.32316q.57149 0 .84666-.21166.27517-.21167.27517-.71967 0-.30691-.11642-.48683-.11642-.1905-.33867-.28575-.21166-.10583-.51858-.13758-.29633-.0318-.66675-.0318h-3.77823v5.01648h3.89465q.35983 0 .66675-.0529.30691-.0529.52916-.16934.22225-.127.33867-.3175.127-.1905.127-.47624 0-.47625-.29633-.73025-.29634-.26458-.86783-.26458z"/></g><g transform="translate(-124.673515 -121.39826)"><path d="m125.48315 122.09239h8.45564v6.59773c0 5.55194-8.57114 5.37087-8.57114 0z" fill="none" stroke="#f9f9f9" stroke-width="1.38827"/><g fill="#f9f9f9" transform="matrix(.33191332 .19163024 -.19163024 .33191332 105.65201 61.486077)"><rect height="6.677884" ry=".917437" width="1.109652" x="133.26457" y="113.53527"/><rect height="6.677884" ry=".917437" width="1.109652" x="145.20386" y="113.53527"/><rect height="14.584357" ry="1.550303" width="1.621357" x="135.81224" y="109.58203"/><rect height="14.584357" ry="1.550303" width="1.621357" x="142.14449" y="109.58203"/><rect height="20.411003" ry="2.16967" width="1.834875" x="138.87161" y="106.66871"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 29 KiB |
2
frontend/src/assets/images/logo.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 24 KiB |
@@ -1,17 +1,29 @@
|
||||
.btn-primary {
|
||||
@apply ring-1 ring-primary-300/95 whitespace-nowrap hover:ring-primary-200 text-left text-sm font-semibold text-primary-300 hover:text-primary-100/90 flex items-center border hover:ring-inset border-primary-300/95 hover:border-primary-200 py-2 px-3 rounded-md hover:shadow justify-center transition-colors duration-100 ease-in-out;
|
||||
@apply ring-1 ring-primary-400/95 whitespace-nowrap focus:ring-primary-300/90 hover:ring-primary-300/80 text-left text-sm font-semibold hover:text-slate-300/70 text-slate-300/80 scale-100 hover:scale-[99%] flex items-center border hover:ring-inset border-primary-400/95 focus:border-primary-300/90 hover:border-primary-300/80 py-2 px-5 font-display rounded-md hover:shadow justify-center transition-all duration-75 ease-linear hover:stroke-primary-300/80 stroke-primary-400/95 ;
|
||||
}
|
||||
|
||||
.btn-primary-solid {
|
||||
@apply ring-1 ring-primary-400/95 hover:ring-primary-300/80 whitespace-nowrap text-left text-sm font-semibold hover:text-slate-200/95 flex scale-100 hover:scale-[99%] items-center hover:ring-inset bg-primary-400/95 hover:bg-primary-300/80 focus:bg-primary-300/90 py-2 px-5 font-display rounded-md hover:shadow justify-center transition-all duration-75 ease-linear text-slate-200/90 hover:text-slate-200 hover:stroke-primary-300/80 stroke-primary-400/95
|
||||
}
|
||||
.btn-primary-solid:disabled {
|
||||
@apply !bg-mirage-400 !ring-mirage-400 !text-slate-500 hover:pointer-events-none
|
||||
}
|
||||
.btn-primary-solid svg,
|
||||
.btn-primary svg,
|
||||
.btn-danger svg {
|
||||
@apply ml-5 w-5 h-5
|
||||
@apply ml-4 w-5 h-5 transition-transform duration-150 ease-out
|
||||
}
|
||||
|
||||
.btn-primary-solid:hover svg,
|
||||
.btn-primary:hover svg,
|
||||
.btn-danger:hover svg {
|
||||
@apply ml-4 w-5 h-5 scale-[106%] !ease-linear
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
@apply ring-1 hover:shadow ring-danger-500/70 hover:ring-danger-500 text-left text-sm font-semibold text-danger-500/70 hover:text-danger-500 flex items-center border hover:ring-inset border-danger-500/70 hover:border-danger-500 py-2 px-3 rounded-md justify-center transition-colors duration-100 ease-in-out
|
||||
@apply ring-1 hover:shadow ring-danger-500/70 hover:ring-danger-500 text-left text-sm font-semibold text-danger-500/70 hover:text-danger-500 scale-100 hover:scale-[99%] flex items-center border hover:ring-inset border-danger-500/70 hover:border-danger-500 py-2 px-3 rounded-md font-display justify-center transition-all duration-75 ease-linear
|
||||
}
|
||||
|
||||
|
||||
.btn-form {
|
||||
@apply flex px-2 py-2 hover:ring-2 focus:ring-1 focus:ring-inset outline-none items-center text-slate-400 hover:text-slate-200 rounded-md ring-1
|
||||
}
|
||||
@@ -22,11 +34,10 @@
|
||||
@apply mx-2
|
||||
}
|
||||
|
||||
|
||||
.form-input-wrapper {
|
||||
@apply mt-2.5 hover:border-mirage-200/40 transition-colors duration-200 ease-in-out block justify-between items-center to-mirage-400/50 from-mirage-300/20 bg-gradient-to-br rounded border focus-within:!border-primary/40 px-3.5 py-1 text-slate-100 shadow-sm border-mirage-400/20 focus-within:from-mirage-400/20 focus-within:to-mirage-400/30 focus-within:bg-gradient-to-l
|
||||
}
|
||||
|
||||
.form-input {
|
||||
@apply block w-full placeholder:text-slate-700 bg-transparent focus:outline-none outline-none sm:text-sm
|
||||
@apply block w-full placeholder:text-slate-700 bg-transparent focus:outline-none outline-none sm:text-sm
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
.bar-hz .ct-label {
|
||||
@apply text-slate-400 font-display
|
||||
}
|
||||
.bar-hz {
|
||||
@apply overflow-y-scroll flex flex-col
|
||||
}
|
||||
|
||||
.bar-hz .ct-series-a .ct-bar {
|
||||
@apply stroke-sky-600
|
||||
@@ -33,4 +36,7 @@
|
||||
}
|
||||
.bar-hz .ct-series-j .ct-bar {
|
||||
@apply stroke-slate-200
|
||||
}
|
||||
|
||||
.ct-label.ct-vertical.ct-end {
|
||||
}
|
||||
@@ -66,7 +66,7 @@
|
||||
color: rgb(203, 213, 225);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
|
||||
font-feature-settings: 'ss01';
|
||||
font-size: 14px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
line-height: 24.5px;
|
||||
tab-size: 4;
|
||||
|
||||
@@ -50,20 +50,28 @@ button {
|
||||
::selection {
|
||||
@apply text-slate-200 bg-radiance-500/50
|
||||
}
|
||||
|
||||
/* 2d6cffb0 */
|
||||
html {
|
||||
background-size: 100% 100%;
|
||||
background-position: 0px 0px,0px 0px,0px 0px,0px 0px,0px 0px;
|
||||
background-image: radial-gradient(49% 81% at 45% 47%, rgba(157, 45, 255, 0.689) 0%, #073AFF00 100%),radial-gradient(113% 91% at 17% -2%, rgba(7, 162, 139, 0.482) 1%, #ff000000 99%),radial-gradient(142% 91% at 83% 7%, rgb(50, 13, 143) 1%, #ff00ea00 99%),radial-gradient(142% 91% at -6% 74%, rgba(0, 42, 255, 0.441) 1%, #FF000000 99%),radial-gradient(142% 91% at 111% 84%, rgb(0, 64, 255) 25%, rgb(69, 34, 150) 95%);
|
||||
background-color: rgb(7, 21, 35, 1);
|
||||
background-image: url(../images/image-bg.siteorigin.com-inflicted-overlay.png );
|
||||
}
|
||||
/* 1hmfchzd.png */
|
||||
|
||||
/* https://gradienta.io/editor */
|
||||
body {
|
||||
@apply min-h-screen from-mirage-800/90 to-mirage-700/50 bg-gradient-to-br;
|
||||
}
|
||||
background-color: transparent;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0px 0px,0px 0px,0px 0px,0px 0px,0px 0px;
|
||||
background-image: radial-gradient(49% 81% at 45% 47%, rgba(71, 0, 80, 0.093) 0%, #073AFF00 100%),radial-gradient(113% 91% at 17% -2%, rgba(7, 70, 132, 0.168) 1%, #ff000000 99%),radial-gradient(142% 91% at 83% 7%, hsla(234, 100%, 19%, 0.109) 1%, rgba(153, 0, 255, 0.126) 99%),radial-gradient(142% 91% at -6% 74%, rgba(0, 42, 255, 0.116) 1%, rgba(3, 154, 84, 0.116) 99%),radial-gradient(142% 91% at 111% 84%, #1144ed0f 25%, rgba(69, 34, 150, 0.045) 95%);}
|
||||
/* https://gradienta.io/editor */
|
||||
|
||||
|
||||
#root {
|
||||
@apply flex flex-col min-h-screen relative bg-mirage-800/60
|
||||
@apply flex flex-col h-full min-h-screen relative bg-mirage-900/20 justify-between;
|
||||
}
|
||||
|
||||
.half-grayscale {
|
||||
--tw-grayscale: grayscale(70%);
|
||||
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
||||
}
|
||||
|
||||
svg.icon {
|
||||
@@ -84,6 +92,7 @@ button,
|
||||
a,
|
||||
figcaption {
|
||||
@apply text-mirage-400 font-display;
|
||||
/* color: #ff9602; */
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -91,23 +100,23 @@ figcaption {
|
||||
Works on Chrome, Edge, and Safari
|
||||
*/
|
||||
*::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
width: 10px;
|
||||
border-radius: none;
|
||||
|
||||
}
|
||||
*::-webkit-scrollbar-track {
|
||||
background: #19233433;
|
||||
width: 8px;
|
||||
width: 10px;
|
||||
border-radius: none;
|
||||
|
||||
}
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: #1a1f2793;
|
||||
border-radius: 2px;
|
||||
border-right: 10px solid #1b243a84;
|
||||
border-right: 9px solid #1b243a84;
|
||||
}
|
||||
* {
|
||||
scrollbar-width: 8px;
|
||||
scrollbar-width: 10px;
|
||||
scrollbar-color: #1d222f62 #202d4333;
|
||||
|
||||
}
|
||||
@@ -200,6 +209,7 @@ input[type='number'] {
|
||||
}
|
||||
.react-flow__attribution a {
|
||||
@apply !text-slate-500;
|
||||
|
||||
}
|
||||
|
||||
.react-flow__edge-path {
|
||||
@@ -293,7 +303,5 @@ input[type='file'] {
|
||||
@apply !border-slate-600 !w-2 !h-2
|
||||
}
|
||||
|
||||
.cm-content {
|
||||
@apply font-code text-sm font-semibold
|
||||
}
|
||||
|
||||
/* end react-grid-layout and codemirror */
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
.sidebar-link {
|
||||
@apply flex items-center transition-colors duration-200 ease-in-out px-2 mx-2 my-1 py-2 text-base font-sans font-medium rounded-md text-slate-600 hover:bg-mirage-100/20 hover:text-slate-500 border hover:border-mirage-400/60 border-transparent hover:from-mirage-500/60 hover:to-mirage-500/40 hover:bg-gradient-to-r;
|
||||
@apply flex items-center duration-150 ease-in-out mx-2.5 my-1 py-2 text-base font-medium rounded-md border hover:border-mirage-400/60 border-transparent hover:ring-slate-800/20 text-slate-400/30 hover:text-slate-400/60 shadow hover:translate-x-px z-50 transition-all focus:text-slate-400/40 -translate-x-px ring-slate-900/20 ring-2 ring-inset from-mirage-400/10 pl-3 to-mirage-300/5 bg-gradient-to-tl from-10% pr-1.5 hover:from-mirage-300/20 hover:to-mirage-300/10 hover:bg-gradient-to-tl font-display;
|
||||
}
|
||||
|
||||
.sidebar-link.active {
|
||||
@apply to-primary-300/30 from-primary-300/40 from-60% border border-mirage-400/60 bg-gradient-to-br shadow-sm bg-mirage-500 text-slate-300 ;
|
||||
@apply hover:translate-x-px shadow translate-x-px ring-1 ring-primary-300/80 border-primary-300/80 ring-inset from-mirage-300/20 to-mirage-300/10 bg-gradient-to-tl from-10% hover:from-mirage-300/20 hover:to-mirage-300/10 hover:bg-gradient-to-tl focus:from-mirage-300/20 focus:to-mirage-300/10;
|
||||
}
|
||||
.sidebar-link.active span {
|
||||
@apply !text-slate-300/80
|
||||
}
|
||||
|
||||
.sidebar-link.active button {
|
||||
@apply bg-mirage-500 hover:bg-mirage-600 border-mirage-300/40 ;
|
||||
@apply bg-mirage-500 border-mirage-300/40 ;
|
||||
}
|
||||
.sidebar-link.active svg {
|
||||
@apply text-slate-400
|
||||
@apply text-primary-300 -ml-0.5
|
||||
}
|
||||
.sidebar-link button {
|
||||
@apply border border-mirage-400/40 transition-colors duration-200 hover:border-mirage-300 bg-mirage-500
|
||||
@apply border border-mirage-400/40 transition-colors duration-150 from-10% hover:from-mirage-300/20 hover:to-mirage-300/10 hover:bg-gradient-to-tl
|
||||
}
|
||||
|
||||
.sidebar-link button svg {
|
||||
@apply text-slate-400
|
||||
@apply origin-center rotate-0
|
||||
}
|
||||
.sidebar-link:hover button svg,
|
||||
.sidebar-link.active button svg {
|
||||
@apply !rotate-180
|
||||
}
|
||||
.sidebar-link button:hover svg {
|
||||
@apply text-slate-300;
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/* Start node */
|
||||
.node.container {
|
||||
@apply flex flex-col w-auto max-w-sm justify-between rounded-md transition-all duration-150 ease-in-out ring-1 ring-mirage-400/30 backdrop-blur-lg hover:ring-mirage-400/70 to-mirage-600/50 from-mirage-500/40 from-60% bg-gradient-to-br hover:shadow-md shadow-sm;
|
||||
@apply flex flex-col w-auto max-w-sm justify-between rounded-md transition-all duration-150 ease-in-out ring-1 ring-mirage-400/90 backdrop-blur-xl focus:ring-mirage-300/70 focus-within:ring-mirage-300/70 hover:ring-mirage-300/70 bg-mirage-500/20 hover:bg-slate-900/60 focus:bg-slate-900/60 focus-within:bg-slate-900/60 hover:shadow-md shadow-slate-900/30 shadow cursor-grab target:cursor-grabbing;
|
||||
}
|
||||
|
||||
.node .side-header {
|
||||
@@ -23,8 +23,6 @@
|
||||
@apply leading-4;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.node .node-display h1,
|
||||
.node .node-display h2,
|
||||
.node .node-display p {
|
||||
@@ -38,11 +36,11 @@
|
||||
}
|
||||
|
||||
.node .header {
|
||||
@apply flex h-full w-full items-center justify-between rounded-t-md text-slate-50/70 py-2 px-1 cursor-grab focus:cursor-grabbing;
|
||||
@apply flex h-full w-full items-center justify-between rounded-t-md text-slate-50/70 py-2 px-1 ;
|
||||
}
|
||||
|
||||
.node .header svg {
|
||||
@apply h-7 w-7;
|
||||
@apply h-6 w-6;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import classNames from "classnames";
|
||||
import { ReactComponent as OSINTBuddyLogo } from "@images/logo.svg";
|
||||
import { ReactComponent as OSINTBuddyLogomark } from "@images/logo-ext.svg"; // OSINTBuddy-text.svg
|
||||
import { Link, NavLink } from "react-router-dom";
|
||||
import HamburgerMenu from "./HamburgerMenu";
|
||||
import { toast } from "react-toastify";
|
||||
import { CogIcon, DocumentMagnifyingGlassIcon, FolderOpenIcon, PlusIcon, HomeIcon } from "@heroicons/react/24/outline";
|
||||
import { Icon } from "./Icons";
|
||||
|
||||
|
||||
const navigation = [
|
||||
{ name: "Dashboard", to: "/dashboard", icon: HomeIcon },
|
||||
{ name: "Incidents *", to: "/incidents", icon: FolderOpenIcon },
|
||||
{ name: "Scans *", to: "/scans", icon: DocumentMagnifyingGlassIcon },
|
||||
{ name: <><span>Dashboard</span></>, to: "/dashboard", icon: HomeIcon },
|
||||
{ name: <><span>Workspaces</span> <span className="half-grayscale ml-auto mr-2.5 opacity-30 right-0">🚧</span></>, to: "/workspaces", icon: FolderOpenIcon },
|
||||
{ name: <><span>Scans</span> <span className="half-grayscale ml-auto mr-2.5 opacity-30 right-0">🚧</span></>, to: "/scans", icon: DocumentMagnifyingGlassIcon },
|
||||
];
|
||||
|
||||
interface AppLayoutSidebarProps {
|
||||
@@ -22,7 +23,7 @@ export default function AppLayoutSidebar({ showSidebar, toggleSidebar, setShowIn
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"fixed inset-y-0 flex border-r from-mirage-900/20 to-mirage-600/20 bg-gradient-to-br shadow border-mirage-800/80 w-64 flex-col transition-transform duration-100",
|
||||
"fixed inset-y-0 flex border-r from-mirage-600/40 to-mirage-600/50 bg-gradient-to-br shadow-xl border-mirage-600/70 w-64 flex-col transition-transform duration-100",
|
||||
showSidebar ? "translate-x-0" : "-translate-x-52 border-r-2"
|
||||
)}
|
||||
>
|
||||
@@ -34,7 +35,7 @@ export default function AppLayoutSidebar({ showSidebar, toggleSidebar, setShowIn
|
||||
)}
|
||||
>
|
||||
<Link to="/" replace>
|
||||
<OSINTBuddyLogo className="h-7 w-auto fill-slate-400" />
|
||||
<OSINTBuddyLogomark className="h-7 ml-1 w-auto fill-slate-400" />
|
||||
</Link>
|
||||
|
||||
<HamburgerMenu
|
||||
@@ -44,86 +45,75 @@ export default function AppLayoutSidebar({ showSidebar, toggleSidebar, setShowIn
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col overflow-y-auto">
|
||||
<nav className="flex-1 flex py-4 pt-1 flex-col">
|
||||
<nav className="flex-1 flex flex-col">
|
||||
{navigation.map((item) => (
|
||||
<NavLink
|
||||
key={item.name}
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive }) =>
|
||||
classNames(
|
||||
isActive && "active",
|
||||
"sidebar-link",
|
||||
!showSidebar && "mx-0"
|
||||
isActive && "active mx-2",
|
||||
"sidebar-link ",
|
||||
!showSidebar ? "mx-0 ml-0 mr-px " : 'mr-2.5'
|
||||
)
|
||||
}
|
||||
>
|
||||
<item.icon
|
||||
className={classNames(
|
||||
"transition-all",
|
||||
location.pathname.includes(item.to)
|
||||
? "text-slate-500"
|
||||
: "",
|
||||
"mr-3 flex-shrink-0 h-6 w-6 duration-100",
|
||||
"transition-all mr-2 flex-shrink-0 h-6 w-6 duration-100",
|
||||
location.pathname.includes(item.to) && "text-slate-400/30",
|
||||
showSidebar
|
||||
? "translate-x-0"
|
||||
: "translate-x-[13.15rem]"
|
||||
: "translate-x-[12.75rem] "
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
{item.name === "Incidents *" && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setShowIncidentsModal(true)
|
||||
toast.info(
|
||||
<>
|
||||
This feature is currently being planned out and
|
||||
created. You can help shape this feature by
|
||||
contributing to the discussion
|
||||
<a
|
||||
className="text-primary"
|
||||
href="https://github.com/jerlendds/osintbuddy/discussions"
|
||||
target="_blank"
|
||||
>
|
||||
on Github!
|
||||
</a>
|
||||
</>,
|
||||
{ autoClose: 10000 }
|
||||
);
|
||||
}}
|
||||
title="Create new incident"
|
||||
className="ml-auto -mr-0.5 relative bg-dark-400 transition-colors duration-75 hover:bg-dark-500 p-1.5 rounded-full"
|
||||
>
|
||||
<PlusIcon className="text-white w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</NavLink>
|
||||
))}
|
||||
<NavLink
|
||||
to="/settings"
|
||||
replace
|
||||
className={({ isActive }) =>
|
||||
className={({ isActive, }) =>
|
||||
classNames(
|
||||
isActive && "active",
|
||||
"sidebar-link mt-auto",
|
||||
!showSidebar && "mx-0"
|
||||
!showSidebar ? "mx-0 ml-0 mr-px" : 'mr-2.5',
|
||||
"sidebar-link transition-all mt-auto sidebar-link text-slate-400/30 ",
|
||||
isActive && "active mx-2"
|
||||
)
|
||||
}
|
||||
>
|
||||
<CogIcon
|
||||
className={classNames(
|
||||
"transition-all",
|
||||
location.pathname.includes("settings")
|
||||
? "text-primary"
|
||||
: "text-slate-600 group-hover:text-slate-300",
|
||||
"mr-3 flex-shrink-0 h-6 w-6 duration-100",
|
||||
showSidebar ? "translate-x-0" : "translate-x-[13.16rem]"
|
||||
"transition-all ",
|
||||
location.pathname.includes("settings") && "!mr-2 hover:-ml-0.5 ",
|
||||
"mr-2 flex-shrink-0 h-6 w-6 duration-100",
|
||||
showSidebar
|
||||
? "translate-x-0"
|
||||
: "translate-x-[12.75rem] "
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Settings
|
||||
<span>Settings</span> <span className="half-grayscale ml-auto mr-2.5 right-0 opacity-30">🚧</span>
|
||||
</NavLink>
|
||||
<a
|
||||
href="https://discord.gg/gsbbYHA3K3"
|
||||
className={
|
||||
classNames(
|
||||
"sidebar-link ",
|
||||
!showSidebar ? "mx-0 ml-0 mr-px" : 'mr-2.5'
|
||||
)
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
icon="brand-discord"
|
||||
className={classNames(
|
||||
"transition-all",
|
||||
"mr-2 flex-shrink-0 h-6 w-6 duration-100",
|
||||
showSidebar
|
||||
? "translate-x-0"
|
||||
: "translate-x-[12.75rem] "
|
||||
)}
|
||||
/>
|
||||
Discord
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,25 +12,36 @@ import { UpdateEntityByIdApiArg } from '../../app/api';
|
||||
import { useParams } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const tokyoNightTheme = auraInit({
|
||||
export const auraTheme = auraInit({
|
||||
settings: {
|
||||
caret: "#c6c6c6",
|
||||
caret: "#cbd5ef",
|
||||
background: 'rgba(0 0 0 0)',
|
||||
fontFamily: 'monospace',
|
||||
gutterBackground: 'rgba(36, 46, 77, .16)',
|
||||
selectionMatch: 'rgba(76, 86, 107, .00)',
|
||||
lineHighlight: 'rgba(36, 46, 77, .26)',
|
||||
fontFamily: 'Fira Code',
|
||||
gutterBackground: 'rgba(36, 46, 107, .17)',
|
||||
selectionMatch: 'rgba(76, 86, 107, .14)',
|
||||
lineHighlight: 'rgba(36, 46, 77, .20)',
|
||||
},
|
||||
styles: [{ tag: t.comment, color: "#6272a4" }],
|
||||
styles: [
|
||||
{ tag: [t.definitionOperator, t.bool, t.logicOperator, t.bitwiseOperator, t.controlOperator,], color: "#ec4899" },
|
||||
{ tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string), t.function(t.propertyName,), t.function(t.variableName),], color: "#2dd4bf" },
|
||||
{ tag: [t.keyword, t.definitionKeyword, t.special(t.keyword), t.attributeValue, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: "#a855f7" },
|
||||
{ tag: [t.variableName, t.deleted, t.character, t.name, t.special(t.variableName)], color: "#cbd5efEF" },
|
||||
{ tag: [t.docString, t.docComment, t.className, t.punctuation,], color: "#49B6FE" },
|
||||
{ tag: [t.propertyName,], color: "#34d399" },
|
||||
{ tag: [t.string], color: "#4ade80" },
|
||||
{ tag: [t.number,], color: "#D8454A" },
|
||||
{ tag: [t.comment, t.lineComment, t.blockComment, t.punctuation], color: "#5a6fbc" },
|
||||
],
|
||||
})
|
||||
|
||||
export function CodeEditor({ code, setCode }: JSONObject) {
|
||||
return (
|
||||
<CodeMirror
|
||||
theme={tokyoNightTheme}
|
||||
theme={auraTheme}
|
||||
value={code}
|
||||
onChange={(value) => setCode(value)}
|
||||
extensions={[python()]}
|
||||
className="text-sm font-semibold"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -65,7 +76,7 @@ export default function EntityEditor({ activeEntity, refetchEntity }: EntityEdit
|
||||
isResizable={true}
|
||||
>
|
||||
<div
|
||||
className="overflow-hidden rounded-md border-mirage-100/0 shadow-lg border z-10 from-mirage-100/10 to-mirage-200/10 bg-gradient-to-tl from-50% flex flex-col h-full"
|
||||
className="overflow-hidden rounded-md border-mirage-100/0 shadow-lg border z-10 backdrop-blur-sm from-mirage-300/20 to-mirage-100/10 bg-gradient-to-bl from-50% flex flex-col h-full"
|
||||
key="b"
|
||||
data-grid={{
|
||||
x: 0,
|
||||
@@ -78,7 +89,7 @@ export default function EntityEditor({ activeEntity, refetchEntity }: EntityEdit
|
||||
minW: 4.5,
|
||||
}}
|
||||
>
|
||||
<ol className="text-sm flex select-none from-mirage-400/30 bg-gradient-to-tr from-40% to-mirage-300/10 relative px-2 py-2 border-b border-mirage-300/80">
|
||||
<ol className="text-sm flex select-none from-mirage-200/20 bg-gradient-to-tr from-40% to-mirage-300/20 relative px-2 py-2 border-b border-mirage-300/80">
|
||||
<li className="flex items-start">
|
||||
<div className="flex items-center">
|
||||
<span className="text-slate-500 font-display truncate">
|
||||
@@ -103,7 +114,7 @@ export default function EntityEditor({ activeEntity, refetchEntity }: EntityEdit
|
||||
<div className="flex justify-between items-center w-full text-slate-400 ">
|
||||
<button
|
||||
onClick={() => {
|
||||
updateEntityById({ hid: activeEntity?.id ?? "", entityBase: { source: code } }).then(() => toast.info(
|
||||
updateEntityById({ hid: activeEntity?.id ?? "", entityUpdate: { source: code as string } }).then(() => toast.info(
|
||||
`The ${activeEntity?.label} entity has been saved.`
|
||||
))
|
||||
refetchEntity()
|
||||
@@ -127,10 +138,8 @@ export default function EntityEditor({ activeEntity, refetchEntity }: EntityEdit
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
<div className="container bg-t overflow-y-scroll h-full ">
|
||||
<div className="editor ">
|
||||
<CodeEditor code={code} setCode={setCode} />
|
||||
</div>
|
||||
<div className="from-mirage-400/20 backdrop-blur-sm bg-gradient-to-tr from-40% to-mirage-400/30 border-mirage-400 border overflow-y-scroll h-full ">
|
||||
<CodeEditor code={code} setCode={setCode} />
|
||||
</div>
|
||||
</div>
|
||||
</ResponsiveGridLayout >
|
||||
|
||||
@@ -27,7 +27,7 @@ export function GridPanel({
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="min-h-[3rem] from-mirage-400/50 to-mirage-400/30 shadow bg-gradient-to-tl from-10% overflow-hidden rounded flex flex-col w-full h-full border border-mirage-400/60 "
|
||||
className="min-h-[3rem] overflow-hidden rounded flex flex-col w-full h-full border border-mirage-400/60 from-mirage-300/30 bg-gradient-to-tr from-40% to-mirage-100/20 shadow "
|
||||
>
|
||||
<ol className="text-sm flex select-none justify-between relative px-2 py-2">
|
||||
<li className="flex items-start">
|
||||
|
||||
@@ -15,7 +15,7 @@ export const InquiryHeader = ({ title, header, className, btnTxt, btnAction }: P
|
||||
<div className='min-w-0 max-w-2xl flex-auto pt-3 lg:max-w-none lg:pr-0 px-2'>
|
||||
<section>
|
||||
<header className='space-y-1'>
|
||||
{title && <h4 className='font-display text-sm font-medium text-primary-300'>{title}</h4>}
|
||||
{title && <h4 className='font-display text-sm font-medium text-primary-200'>{title}</h4>}
|
||||
{header && (
|
||||
<h1 className='font-display leading-4 text-xl tracking-tight text-slate-400 dark:text-slate-300'>{header}</h1>
|
||||
)}
|
||||
|
||||
@@ -2,12 +2,10 @@ import classNames from 'classnames';
|
||||
import Sprite from '@assets/images/tabler-sprite.svg';
|
||||
|
||||
export const Icon = ({ icon, className }: { icon: string, className?: string }) => {
|
||||
// TODO: Remove sprite and figure this approach out somehow
|
||||
// const svgIcon = import(`@tabler/icons/${icon}.svg`).then(x => x).catch(() => <>why no work?!</>)
|
||||
|
||||
// TODO: Optimize this?
|
||||
return (
|
||||
<>
|
||||
<svg className={className ? className : 'h-5 w-5'} fill='#fff' color='#fff'>
|
||||
<svg className={className ? className : 'h-5 w-5'} fill="none" stroke="currentColor">
|
||||
<use href={`${Sprite}#tabler-${icon}`} />
|
||||
</svg>
|
||||
</>
|
||||
|
||||
@@ -23,16 +23,18 @@ export default function RoundLoader({ className }: RoundLoaderProps): React.Reac
|
||||
export function UnderConstruction({ header, description, className = 'flex px-6' }: any) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className='bg-mirage-700/50 w-full block shadow sm:rounded-lg '>
|
||||
<div className='bg-mirage-800/30 w-full block shadow rounded-b-lg '>
|
||||
<div className='border-b border-mirage-300 mx-4 py-5 sm:px-6'>
|
||||
<div className='-ml-6 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap'>
|
||||
<div className='ml-4 mt-2'>
|
||||
<h1 className='font-display text-2xl tracking-tight text-slate-300'>
|
||||
Under Construction
|
||||
<div className='ml-4 mt-2 w-full'>
|
||||
<h1 className='text-2xl w-full flex items-center justify-between'>
|
||||
<span className="half-grayscale mr-2.5 opacity-30 right-0">🚧</span><p className="font-display font-semibold text-slate-300/90 mr-auto"> Under Construction</p>
|
||||
<span className="half-grayscale opacity-30 right-0 ml-auto">🚧</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className='flex flex-col px-3 mb-6'>
|
||||
<InquiryHeader className='z-10' title='New Feature' header={header} />
|
||||
<p className='my-3 ml-6 text-slate-400 max-w-xl'>
|
||||
|
||||
@@ -5,8 +5,8 @@ import { Bars3Icon, BellIcon, XMarkIcon } from '@heroicons/react/24/outline';
|
||||
import classNames from 'classnames';
|
||||
import OSINTBuddyLogo from '@assets/images/logo.svg';
|
||||
import { Listbox } from '@headlessui/react'
|
||||
import { GithubIcon } from './Icons';
|
||||
import Logo from '@images/logo.svg';
|
||||
import { Icon } from './Icons';
|
||||
import Logo from '@images/logo-ext.svg';
|
||||
|
||||
const themes = [
|
||||
{ name: 'Light', value: 'light', icon: LightIcon },
|
||||
@@ -60,15 +60,15 @@ export function ThemeSelector(props: any) {
|
||||
} else {
|
||||
setSelectedTheme(
|
||||
// @ts-ignore
|
||||
themes.find((theme) =>theme.value === document.documentElement.getAttribute('data-theme')))
|
||||
themes.find((theme) => theme.value === document.documentElement.getAttribute('data-theme')))
|
||||
}
|
||||
}, [selectedTheme])
|
||||
|
||||
useEffect(() => {
|
||||
let handler = () =>
|
||||
|
||||
|
||||
setSelectedTheme(
|
||||
// @ts-ignore
|
||||
// @ts-ignore
|
||||
themes.find((theme) => theme.value === (window.localStorage.theme ?? 'system')))
|
||||
|
||||
window.addEventListener('storage', handler)
|
||||
@@ -147,28 +147,36 @@ export default function PublicNavbar(): React.ReactElement {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<header
|
||||
<header
|
||||
className={classNames(
|
||||
'fixed w-full top-0 z-50 flex flex-wrap items-center justify-between px-4 py-5 shadow-md shadow-slate-900/5 transition duration-500 dark:shadow-none sm:px-6 lg:px-8',
|
||||
'sticky w-full top-0 z-50 flex sm:flex-wrap items-center justify-between flex-nowrap px-4 py-2.5 shadow-md shadow-slate-900/5 transition duration-500 dark:shadow-none sm:px-6 lg:px-8',
|
||||
isScrolled
|
||||
? 'dark:bg-slate-900/95 dark:backdrop-blur dark:[@supports(backdrop-filter:blur(0))]:bg-slate-900/75'
|
||||
? 'dark:bg-slate-800/95 dark:backdrop-blur dark:[@supports(backdrop-filter:blur(0))]:bg-slate-900/75'
|
||||
: 'dark:bg-transparent'
|
||||
)}
|
||||
>
|
||||
<div className="mr-6 flex lg:hidden">
|
||||
<div className="flex lg:hidden">
|
||||
<Link to="/">
|
||||
<img className=" h-7 w-auto fill-slate-700 dark:fill-sky-100 block" src={Logo} />
|
||||
</Link>
|
||||
{/* @todo add mobile navigation */}
|
||||
</div>
|
||||
<div className="relative flex flex-grow basis-0 items-center">
|
||||
<Link to="/">
|
||||
<img className="hidden h-9 w-auto fill-slate-700 dark:fill-sky-100 lg:block" src={Logo} />
|
||||
<img className="hidden h-9 fill-slate-700 dark:fill-sky-100 lg:block" src={Logo} />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="-my-5 mr-6 sm:mr-8 md:mr-0">
|
||||
<div className="">
|
||||
{/* <Search /> */}
|
||||
</div>
|
||||
<div className="relative flex basis-0 justify-end gap-6 sm:gap-8 md:flex-grow">
|
||||
<div className="ml-auto relative flex basis-0 justify-end mr-2 gap-6 sm:gap-8 md:flex-grow">
|
||||
<a href="https://discord.gg/gsbbYHA3K3" className="group" aria-label="Discord">
|
||||
<Icon icon="brand-discord" className="h-6 w-6 transition-colors duration-150 ease-in-out text-slate-500 hover:text-slate-300 focus:text-slate-300" />
|
||||
</a>
|
||||
</div>
|
||||
<div className="relative flex justify-end">
|
||||
<a href="https://github.com/jerlendds/osintbuddy" className="group" aria-label="GitHub">
|
||||
<GithubIcon className="h-6 w-6 fill-slate-400 group-hover:fill-slate-500 dark:group-hover:fill-slate-300" />
|
||||
<Icon icon="brand-github" className="h-6 w-6 transition-colors duration-150 ease-in-out text-slate-500 hover:text-slate-300 focus:text-slate-300" />
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -24,6 +24,8 @@ export interface Graph extends EditState {
|
||||
project: ActiveProject;
|
||||
viewMode: ProjectViewModes;
|
||||
positionMode: PositionModes
|
||||
showEdges: false,
|
||||
shownEdgesByNodeId: JSONObject[]
|
||||
}
|
||||
|
||||
const initialState: Graph = {
|
||||
@@ -39,7 +41,9 @@ const initialState: Graph = {
|
||||
name: '',
|
||||
},
|
||||
viewMode: 'edit',
|
||||
positionMode: 'manual'
|
||||
positionMode: 'manual',
|
||||
showEdges: false,
|
||||
shownEdgesByNodeId: []
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function AppLayout() {
|
||||
showSidebar ? "translate-x-64" : "translate-x-12"
|
||||
)}
|
||||
>
|
||||
<main id="main-view" className="flex-1 overflow-hidden h-screen">
|
||||
<main id="main-view" className="flex-1 overflow-hidden h-screen ">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactElement, Suspense, lazy } from "react";
|
||||
import { Route, RouterProvider, Routes, createBrowserRouter } from "react-router-dom";
|
||||
import { Navigate, Route, RouterProvider, Routes, createBrowserRouter } from "react-router-dom";
|
||||
import NotFound from "./NotFound";
|
||||
import AppLayout from "./AppLayout";
|
||||
import PublicLayout from "./PublicLayout";
|
||||
@@ -24,7 +24,7 @@ const EntitiesOverview = lazy(() => import("@routes/dashboard/_components/entity
|
||||
const EntityDetails = lazy(() => import("@routes/dashboard/_components/entity/EntityDetails"))
|
||||
|
||||
const Settings = lazy(() => import("@routes/settings"));
|
||||
const Incidents = lazy(() => import("@routes/incidents"));
|
||||
const Workspaces = lazy(() => import("@src/routes/workspaces"));
|
||||
|
||||
const ScansCreate = lazy(() => import("@routes/scans-create"))
|
||||
const ScansOverview = lazy(() => import("@routes/scans"));
|
||||
@@ -58,6 +58,10 @@ const router = createBrowserRouter([
|
||||
path: 'dashboard',
|
||||
element: <Suspense><DashboardPage /></Suspense>,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
element: <Navigate to="graph" replace />
|
||||
},
|
||||
{
|
||||
path: 'market',
|
||||
element: <Suspense><Market /></Suspense>
|
||||
@@ -89,8 +93,8 @@ const router = createBrowserRouter([
|
||||
element: <Suspense><Settings /></Suspense>
|
||||
},
|
||||
{
|
||||
path: 'incidents',
|
||||
element: <Suspense><Incidents /></Suspense>
|
||||
path: 'workspaces',
|
||||
element: <Suspense><Workspaces /></Suspense>
|
||||
},
|
||||
{
|
||||
path: 'scans',
|
||||
|
||||
@@ -6,7 +6,9 @@ export default function PublicLayout(): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
<PublicNavbar />
|
||||
<Outlet />
|
||||
<main id="main-view">
|
||||
<Outlet />
|
||||
</main>
|
||||
<ToastContainer
|
||||
position='bottom-right'
|
||||
autoClose={3000}
|
||||
|
||||
@@ -7,18 +7,18 @@ import { FetchBaseQueryError } from "@reduxjs/toolkit/query"
|
||||
import { SerializedError } from "@reduxjs/toolkit"
|
||||
|
||||
|
||||
const MAX_DESCRIPTION_LENGTH = 63
|
||||
const MAX_LABEL_LENGTH = 31
|
||||
const MAX_DESCRIPTION_LENGTH = 79
|
||||
const MAX_LABEL_LENGTH = 32
|
||||
|
||||
export function GraphLoaderCard() {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2">
|
||||
<div className="w-full py-6 space-y-5 rounded-md rounded-r-none bg-mirage-400/30 before:absolute px-4 bg-gradient-to-r from-transparent via-mirage-400/10 to-transparent relative before:inset-0 before:bg-gradient-to-r before:from-transparent before:via-mirage-300/5 before:to-transparent isolate overflow-hidden shadow-xl shadow-black/5 border-l border-y border-mirage-500">
|
||||
<div className="w-full py-6 space-y-1 rounded-md rounded-r-none border border-mirage-400/60 from-mirage-400/50 to-mirage-400/30 shadow bg-gradient-to-tl from-10% before:absolute px-4 via-mirage-400/10 relative before:inset-0 before:bg-gradient-to-r before:from-transparent before:via-mirage-300/5 before:to-transparent isolate overflow-hidden shadow-black/5 border-l border-y border-mirage-500">
|
||||
<div className="space-y-3">
|
||||
<div className="h-2 w-3/5 rounded-lg bg-mirage-500/60 animate-pulse"></div>
|
||||
<div className="h-2 w-4/5 rounded-lg bg-mirage-500/60 animate-pulse"></div>
|
||||
<div className="h-2 w-2/5 rounded-lg bg-mirage-500/60 animate-pulse"></div>
|
||||
<div className="h-2 w-3/5 rounded-lg bg-slate-600/20 animate-pulse"></div>
|
||||
<div className="h-2 w-4/5 rounded-lg bg-slate-600/20 animate-pulse"></div>
|
||||
<div className="h-2 w-2/5 rounded-lg bg-slate-600/20 animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,16 +54,16 @@ export default function Subpanel({
|
||||
const { hid } = useParams();
|
||||
|
||||
return (
|
||||
<div className={styles["subpanel"]}>
|
||||
<div className={styles["subpanel-header"]} onClick={setShowEntities}>
|
||||
<p>{label ?? ""}</p>
|
||||
<ChevronDownIcon className={styles[`show-header-icon-${showEntities}`]} />
|
||||
</div>
|
||||
<section className={styles["subpanel"]}>
|
||||
<header className={styles["subpanel-header"]} onClick={setShowEntities}>
|
||||
<h2 className={`${styles["cyberpunk"]} ${styles["glitched"]}`}>{label ?? ""}</h2>
|
||||
<ChevronDownIcon className={styles[`show-header-icon-${showEntities}`] + " transition-transform duration-100"} />
|
||||
</header>
|
||||
{showError && showEntities && !isLoading && (
|
||||
<>
|
||||
<p>
|
||||
{errorMessage?.length ? (<>{errorMessage}</>) : (
|
||||
<>We ran into an error retrieving your entities. Please try refreshing the page, if this error continues to occur please <a href="#" className="text-info-300">file an issue</a> on github</>
|
||||
{errorMessage?.length ? (<p className="text-slate-400/60 text-sm px-2">{errorMessage}</p>) : (
|
||||
<>We ran into an error fetching your data. Please try refreshing the page, if this error continues to occur please <a href="#" className="text-info-300">file an issue</a> on github</>
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
@@ -74,32 +74,33 @@ export default function Subpanel({
|
||||
<GraphLoaderCard />
|
||||
</>
|
||||
)}
|
||||
{isSuccess && showEntities && (
|
||||
<section className={styles["subpanel-section"]}>
|
||||
{items && items.map((item) => {
|
||||
return (
|
||||
<Link
|
||||
key={item.id}
|
||||
to={`${to}/${item.id}`}
|
||||
className={styles["subpanel-link"] + " " + styles[`subpanel-link-${hid === item.id}`]}>
|
||||
<div>
|
||||
<p className={styles["subpanel-label"] + " " + styles[`subpanel-label-${hid === item.id}`]}>{item?.label.length > MAX_LABEL_LENGTH ? `${item.label.slice(0, MAX_LABEL_LENGTH)}...` : item.label}</p>
|
||||
<p className={styles["subpanel-desc"] + " " + styles[`subpanel-desc-${hid === item.id}`]}>{item?.description?.length ?? 0 >= MAX_DESCRIPTION_LENGTH ? `${item?.description?.slice(0, MAX_DESCRIPTION_LENGTH)}...` : item.description}</p>
|
||||
<p className={styles["subpanel-desc"] + " " + styles[`subpanel-desc-${hid === item.id}`]}>Last seen {formatPGDate(item?.last_edited ? item.last_edited : item.last_seen)}</p>
|
||||
</div>
|
||||
<StarIcon
|
||||
onClick={async () => await onClick(item.id)}
|
||||
className={
|
||||
styles["link-icon"] + " " +
|
||||
styles[`link-icon-${item.is_favorite}`] + " " +
|
||||
styles[`link-active-${hid !== item.id}`]
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<section className={` transition-transform duration-150 ease-out ${showEntities ? 'translate-y-0' : '-translate-y-[45%] -scale-y-0 !h-0'}`}>
|
||||
{items && items.map((item) => {
|
||||
const isActive = hid === item.id
|
||||
const descriptionClassName = styles["subpanel-desc"] + " " + styles[`subpanel-desc-${isActive}`]
|
||||
return (
|
||||
<Link
|
||||
key={item.id}
|
||||
to={`${to}/${item.id}`}
|
||||
className={`${styles["subpanel-link"]} ${styles[`subpanel-link-${isActive}`]} ${isSuccess && showEntities ? 'translate-y-0 scale-y-100' : '-translate-y-full scale-y-0 '}`}>
|
||||
<div>
|
||||
<p className={styles["subpanel-label"] + " " + styles[`subpanel-label-${isActive}`]}>{item.label}</p>
|
||||
<p className={descriptionClassName}>{item.description}</p>
|
||||
<p className={descriptionClassName}><span className="font-sans">Last seen </span> {formatPGDate(item?.last_edited ? item.last_edited : item.last_seen)}</p>
|
||||
</div>
|
||||
<StarIcon
|
||||
onClick={async () => await onClick(item.id)}
|
||||
className={
|
||||
styles["link-icon"] + " " +
|
||||
styles[`link-icon-${item.is_favorite}`] + " " +
|
||||
styles[`link-active-${hid !== item.id}`]
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</section>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -92,9 +92,9 @@ export default function GraphDetails() {
|
||||
return (
|
||||
<>
|
||||
<section className="flex flex-col h-screen w-full">
|
||||
<div className="flex w-full">
|
||||
<header className="flex w-full">
|
||||
<GraphHeader refetchGraphs={async () => await refetchGraphs()} stats={graphStats} graph={activeGraph as Graph} />
|
||||
</div>
|
||||
</header>
|
||||
<ResponsiveGridLayout
|
||||
className="relative flex-grow h-full z-10 w-full"
|
||||
rowHeight={1}
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function GraphHeader({ graph, stats, refetchGraphs }: GraphHeader
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex w-full border-b-2 border-mirage-400/20 -top-[2px] relative">
|
||||
<div className="flex w-full border-b-2 bg-mirage-800/25 border-mirage-400/20 relative">
|
||||
<div className=" w-full mx-auto">
|
||||
<section className="flex flex-col items-start justify-between px-4 lg:items-center lg:px-6 md:px-4 lg:flex-row md:flex-row md:items-center relative">
|
||||
<div className="flex flex-col w-full py-1">
|
||||
@@ -49,15 +49,15 @@ export default function GraphHeader({ graph, stats, refetchGraphs }: GraphHeader
|
||||
</button>
|
||||
<Link
|
||||
to={`/graph/inquiry/${graph?.id}`}
|
||||
className='btn-primary mb-3.5'
|
||||
className='btn-primary mb-3.5 stroke-primary-300/90 hover:!stroke-primary-400/80'
|
||||
>
|
||||
Open graph
|
||||
<EyeIcon className='ml-2 w-5 h-5 ' />
|
||||
<EyeIcon className='ml-2 w-5 h-5 stroke-inherit' />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
<hr className=" border-b border-mirage-400/20" />
|
||||
<section className=" bg-mirage-800/25 flex flex-col px-4 py-2 lg:px-6 md:px-4 gap-x-5 lg:flex-row md:flex-row gap-y-4">
|
||||
<section className=" from-mirage-600/20 to-mirage-600/20 bg-gradient-to-l from-10% flex flex-col px-4 py-2 lg:px-6 md:px-4 gap-x-5 lg:flex-row md:flex-row gap-y-4">
|
||||
{graph && stats && (
|
||||
<>
|
||||
<div className="flex items-center ml-[4px] gap-x-2">
|
||||
|
||||
@@ -18,13 +18,13 @@ export default function GraphOverview() {
|
||||
}, [graphsData])
|
||||
|
||||
return <>
|
||||
<div className="w-full items-center justify-center my-14">
|
||||
<div className="grid place-items-center relative z-10 text-slate-400">
|
||||
<div className="from-mirage-400/30 bg-gradient-to-tr from-40% to-mirage-300/10 border-mirage-400 border rounded-md grid place-items-center my-8 py-16 px-4 md:px-16 lg:px-20">
|
||||
<div className="w-full items-center justify-center my-auto relative -top-16">
|
||||
<div className="flex flex-col items-center justify-center text-slate-400">
|
||||
<div className="max-w-2xl w-full from-mirage-300/30 bg-gradient-to-tr from-40% to-mirage-100/20 border-mirage-400 border rounded-md grid place-items-start py-16 px-24 ">
|
||||
{favoriteGraphs.length === 0 && graphs.length === 0 && (
|
||||
<>
|
||||
<h1 className="text-slate-300 text-3xl lg:text-4xl font-bold pt-12 sm:pt-14 lg:pt-8 ">Oh no!</h1>
|
||||
<p className="py-6 md:py-8 max-w-lg">
|
||||
<h1 className="text-slate-300/80 text-3xl lg:text-4xl font-bold pr-2">Oh no!</h1>
|
||||
<p className="md:pt-4 max-w-xl text-slate-300/80">
|
||||
We're usually a treasure chest of knowledge,
|
||||
but we couldn't find any graphs. Get started
|
||||
by creating a new graph
|
||||
@@ -33,15 +33,22 @@ export default function GraphOverview() {
|
||||
)}
|
||||
{(graphs.length > 0 || favoriteGraphs.length > 0) && (
|
||||
<>
|
||||
<h1 className="text-slate-300 text-3xl lg:text-4xl font-bold pt-12 sm:pt-14 lg:pt-8 ">
|
||||
<h1 className="text-slate-300/80 text-3xl lg:text-4xl font-bold pr-2">
|
||||
{graphs.length + favoriteGraphs.length ?? ''} {favoriteGraphs.length + graphs.length > 1 ? 'graphs' : 'graph'} available</h1>
|
||||
<p className="py-6 md:py-8 max-w-md">
|
||||
<p className="md:pt-4 max-w-xl text-slate-300/80">
|
||||
Get started by selecting an existing graph from the sidebar
|
||||
on the left, or try creating a new graph
|
||||
on the left or you can create a new graph with the button located towards the bottom left
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-w-2xl w-full from-mirage-300/30 bg-gradient-to-tr from-40% to-mirage-100/20 border-mirage-400 border rounded-md grid place-items-start my-5 py-10 mt-8 px-24">
|
||||
<h1 className="text-slate-300/80 text-3xl lg:text-4xl font-bold border-b-2 pr-2 border-b-primary-300/80">The availability bias </h1>
|
||||
<p className="md:py-4 max-w-xl text-slate-300/80">
|
||||
Remember, data presentation can impact results during exploration and analysis. Availability bias is the tendency to think readily available examples are more representative than they really are. Even color can influence interpretation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
.subpanel section {
|
||||
@apply py-1 flex flex-col h-min relative z-0 items-start justify-start
|
||||
}
|
||||
/* refactor me */
|
||||
.subpanel-wrapper {
|
||||
@apply flex flex-col justify-between relative flex-shrink h-full pr-2;
|
||||
@apply flex flex-col justify-between relative flex-shrink overflow-clip;
|
||||
}
|
||||
.subpanel-section {
|
||||
@apply py-1 px-1 flex flex-col;
|
||||
}
|
||||
.subpanel > p {
|
||||
.subpanel > h2 {
|
||||
@apply text-sm py-4 font-medium text-slate-500 focus:outline-none cursor-pointer px-2
|
||||
}
|
||||
|
||||
.subpanel-header {
|
||||
@apply flex pb-1 items-center hover:border-mirage-400/60 border-b transition-colors duration-100 ease-in-out border-transparent
|
||||
@apply flex py-0.5 pb-1 items-center hover:text-slate-500 text-slate-600 hover:border-primary-300/40 border-mirage-400/60 border-b transition-colors duration-300 ease-in-out z-50
|
||||
}
|
||||
.subpanel-header p {
|
||||
@apply text-sm px-1 font-black leading-3 text-slate-600 focus:outline-none cursor-pointer
|
||||
.subpanel-header h2 {
|
||||
@apply text-sm px-1 font-semibold font-display leading-3 select-none text-inherit focus:outline-none cursor-pointer
|
||||
}
|
||||
.subpanel-header svg {
|
||||
@apply h-5 w-5 mr-2 hover:rotate-3 origin-center text-slate-600 ml-auto transform focus:outline-none cursor-pointer
|
||||
@apply h-5 w-5 mr-1 hover:rotate-3 origin-center text-slate-600 ml-auto transform focus:outline-none cursor-pointer
|
||||
}
|
||||
.subpanel-header:hover svg {
|
||||
@apply border-primary-300/40
|
||||
}
|
||||
|
||||
.show-header-icon-true {
|
||||
@apply !-rotate-180
|
||||
}
|
||||
@@ -27,36 +30,46 @@
|
||||
}
|
||||
|
||||
.subpanel-link {
|
||||
@apply mb-1 focus:outline-none bg-transparent border rounded-md border-transparent hover:to-mirage-400/20 hover:bg-gradient-to-bl hover:border-mirage-700/70 hover:translate-x-px z-50 transition-transform hover:from-mirage-400/10 text-slate-400 hover:text-slate-400/20 focus:bg-mirage-500/60 focus:text-slate-400/40 w-full flex items-center -translate-x-px
|
||||
@apply mb-1 focus:outline-none bg-transparent border rounded-md border-transparent
|
||||
hover:ring-slate-700/20 hover:to-mirage-400/30 shadow hover:bg-gradient-to-tl from-10% hover:translate-x-px z-0 transition-transform hover:from-mirage-400/10 w-full flex items-center -translate-x-px relative duration-100 ease-out overflow-hidden
|
||||
}
|
||||
|
||||
|
||||
.subpanel-link div:hover .subpanel-label {
|
||||
@apply !text-slate-400/95
|
||||
@apply !text-slate-400/95
|
||||
}
|
||||
|
||||
.subpanel-link-true {
|
||||
@apply !border-mirage-300/30 from-mirage-400/10 to-mirage-400/30 hover:from-mirage-400/10 hover:to-mirage-400/30 bg-gradient-to-br hover:bg-gradient-to-br hover:translate-x-0 shadow translate-x-0
|
||||
@apply hover:-translate-x-px shadow -translate-x-px ring-slate-600/10 ring-1 ring-inset from-mirage-300/20 to-mirage-300/10 bg-gradient-to-tl from-10% hover:from-mirage-300/20 hover:to-mirage-300/10 hover:bg-gradient-to-tl
|
||||
}
|
||||
|
||||
.subpanel-link div {
|
||||
@apply flex w-full p-3 flex-col items-start space-y-1.5 text-slate-400/30 hover:text-slate-400/60
|
||||
@apply flex w-full p-2.5 pr-0 flex-col items-start mb-0.5 text-slate-400/40 hover:text-slate-400/60
|
||||
}
|
||||
|
||||
|
||||
|
||||
.subpanel-link div p:nth-child(2) {
|
||||
@apply max-h-8 line-clamp-2 leading-4 w-full pr-2 h-full
|
||||
}
|
||||
.subpanel-link div p:last-child {
|
||||
@apply mt-1
|
||||
}
|
||||
svg.link-icon {
|
||||
@apply bg-mirage-500/80 border-transparent border ml-auto rounded-md p-1 h-8 w-8 transition-colors duration-150 hover:text-primary-300 hover:border-mirage-300/20 mr-2
|
||||
@apply bg-slate-800/20 border-transparent border ml-auto rounded p-1 h-8 w-8 transition-colors duration-150 hover:text-primary-300 hover:border-mirage-300/20
|
||||
}
|
||||
svg.link-icon-true {
|
||||
@apply !text-primary-300 hover:!text-primary-200;
|
||||
@apply !text-primary-300 hover:!text-primary-200 !bg-transparent;
|
||||
}
|
||||
svg.link-icon-false {
|
||||
@apply text-slate-600
|
||||
}
|
||||
.link-active-true {
|
||||
@apply !bg-transparent hover:!bg-slate-900 ;
|
||||
@apply !bg-transparent hover:!bg-slate-800/40 transition-colors duration-200 ;
|
||||
}
|
||||
|
||||
.subpanel-label {
|
||||
@apply text-sm font-medium leading-none
|
||||
@apply text-sm leading-4 font-display font-medium line-clamp-1 pb-0.5
|
||||
}
|
||||
.subpanel-label-true {
|
||||
@apply text-slate-400/95;
|
||||
@@ -65,8 +78,22 @@ svg.link-icon-false {
|
||||
@apply text-slate-500/60;
|
||||
}
|
||||
.subpanel-desc {
|
||||
@apply text-xs font-medium leading-none text-left;
|
||||
@apply text-xs font-display font-light leading-none text-left;
|
||||
}
|
||||
.subpanel-desc-true {
|
||||
@apply !text-slate-400/60
|
||||
}
|
||||
@apply !text-slate-400/80
|
||||
}
|
||||
|
||||
|
||||
/* .cyberpunk {
|
||||
width: calc(100% - 30px);
|
||||
border: 30px solid #000;
|
||||
border-left: 5px solid #000;
|
||||
border-right: 5px solid #000;
|
||||
border-bottom: 15px solid #000;
|
||||
clip-path: polygon(0px 25px, 26px 0px, calc(60% - 25px) 0px, 60% 25px, 100% 25px, 100% calc(100% - 10px), calc(100% - 15px) calc(100% - 10px), calc(80% - 10px) calc(100% - 10px), calc(80% - 15px) calc(100% - 0px), 10px calc(100% - 0px), 0% calc(100% - 10px));
|
||||
margin-bottom: 10px;
|
||||
padding: 12px;
|
||||
font-size: 1.7rem;
|
||||
font-weight: 700;
|
||||
} */
|
||||
|
||||
@@ -30,8 +30,8 @@ export default function EntitiesPanel({
|
||||
|
||||
const [updateEntityIsFavorite] = useUpdateEntityFavoriteIdMutation()
|
||||
|
||||
const updateEntityOnBookmark = async (hid: string) => {
|
||||
await updateEntityIsFavorite({ hid })
|
||||
const updateEntityOnFavorite = (hid: string) => {
|
||||
updateEntityIsFavorite({ hid })
|
||||
refetchEntities()
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function EntitiesPanel({
|
||||
isLoading={isLoading}
|
||||
isSuccess={isSuccess}
|
||||
items={favoriteEntities}
|
||||
onClick={async (hid) => await updateEntityOnBookmark(hid)}
|
||||
onClick={(hid) => updateEntityOnFavorite(hid)}
|
||||
to="/dashboard/entity"
|
||||
/>
|
||||
<Subpanel
|
||||
@@ -56,7 +56,7 @@ export default function EntitiesPanel({
|
||||
isLoading={isLoading}
|
||||
isSuccess={isSuccess}
|
||||
items={entities}
|
||||
onClick={async (hid) => await updateEntityOnBookmark(hid)}
|
||||
onClick={(hid) => updateEntityOnFavorite(hid)}
|
||||
to="/dashboard/entity"
|
||||
/>
|
||||
</section >
|
||||
|
||||
@@ -1,51 +1,57 @@
|
||||
/* wraps the sidebar with search and the main view */
|
||||
.sidebar-wrapper {
|
||||
@apply rounded py-px !min-w-[20rem] max-w-[20rem] flex-col h-screen pt-3.5 border-r-[3px] border-mirage-700/70 from-mirage-600/20 to-mirage-500/20 bg-gradient-to-br pl-2 justify-between shadow-xl flex relative w-full overflow-y-scroll
|
||||
@apply rounded py-px !min-w-[20rem] max-w-[20rem] flex-col h-screen pt-3.5 border-r-[3px] from-mirage-600/40 to-mirage-600/50 bg-gradient-to-br border-mirage-600/40 justify-between shadow-xl flex relative w-full
|
||||
}
|
||||
|
||||
.search-container {
|
||||
@apply flex items-center relative border transition-colors duration-200 ease-out from-mirage-400/60 to-mirage-300/20 bg-gradient-to-tl rounded border-mirage-300/30 hover:border-mirage-300 mb-2 mr-2 ml-1
|
||||
@apply flex mx-1.5 items-center relative border text-slate-600 hover:text-slate-300/40 focus:text-slate-300/40 transition-all duration-200 ease-out from-mirage-400/60 to-mirage-300/20 bg-gradient-to-tl rounded border-mirage-300/50 hover:border-mirage-300 mb-1.5
|
||||
}
|
||||
.search-container:active, .search-container:focus-within {
|
||||
@apply border-primary/50 to-mirage-400/10 from-mirage-400/20 bg-gradient-to-l
|
||||
}
|
||||
|
||||
.search-container input {
|
||||
@apply py-1.5 pl-1.5 w-full focus:outline-none relative text-sm font-medium text-slate-400 bg-transparent placeholder-slate-600
|
||||
@apply py-1.5 pl-1.5 w-full focus:outline-none relative text-sm font-medium text-slate-400 bg-transparent placeholder-slate-600
|
||||
}
|
||||
|
||||
.search-container svg {
|
||||
@apply w-5 h-5 text-slate-700 ml-2
|
||||
@apply w-5 h-5 text-inherit ml-2
|
||||
}
|
||||
|
||||
.dashboard-tabs {
|
||||
@apply overflow-y-hidden flex-grow h-full pb-12
|
||||
@apply overflow-y-hidden flex-grow flex flex-col items-stretch
|
||||
}
|
||||
|
||||
.tabs-list {
|
||||
@apply flex justify-between w-full rounded p-1 ;
|
||||
@apply flex justify-between rounded pb-2 pt-0.5 items-center;
|
||||
}
|
||||
.tab {
|
||||
@apply focus:outline-none transition-colors duration-200 ease-in-out flex items-center justify-center py-2.5 px-0 font-display rounded min-w-[6rem] text-sm leading-none z-20
|
||||
@apply focus:outline-none transition-colors duration-200 ease-in-out flex items-center justify-center pt-2.5 px-0 mx-0 rounded min-w-[6.5rem] text-sm leading-none z-[1] font-display flex-grow hover:pointer-events-auto
|
||||
}
|
||||
.tab span,
|
||||
.tab-false span,
|
||||
.tab-true span {
|
||||
@apply min-w-min
|
||||
}
|
||||
.tab-true {
|
||||
@apply text-slate-300 ;
|
||||
@apply text-slate-300/90 ;
|
||||
}
|
||||
.tab-false {
|
||||
@apply text-slate-600 hover:text-slate-500;
|
||||
@apply text-slate-600 hover:text-slate-500 ;
|
||||
}
|
||||
.tab-slider {
|
||||
@apply from-primary-300/40 to-primary-300/60 border border-mirage-400/60 bg-gradient-to-br h-[34px] translate-x-0 w-[90px] absolute transition-transform duration-200 ease-in-out rounded
|
||||
@apply min-h-[34px] mr-auto min-w-[86px] left-0 relative z-[0] top-1 hover:scale-x-[100%] transition-all duration-200 ease-in-out rounded scale-x-[104%] from-primary-300/40 to-primary-300/60 border border-mirage-400/60 bg-gradient-to-br;
|
||||
/* clip-path: polygon(0px 0px, 0px 26px, 9px 36px, 90px 36px, 90px 0px, 0px 0px, 0px 0px ); */
|
||||
}
|
||||
.graph-tab[aria-selected="true"] ~ div {
|
||||
@apply translate-x-[0px] w-[98px]
|
||||
@apply -translate-x-[300px] scale-x-[104%] cursor-pointer
|
||||
}
|
||||
.entities-tab[aria-selected="true"] ~ div {
|
||||
@apply translate-x-[101px] w-[100px]
|
||||
@apply -translate-x-[200px] scale-x-[110%] cursor-pointer
|
||||
}
|
||||
.market-tab[aria-selected="true"] ~ div {
|
||||
@apply translate-x-[205px] w-[93px]
|
||||
@apply -translate-x-[95px] scale-x-105 cursor-pointer
|
||||
}
|
||||
.tab-panel {
|
||||
@apply w-full relative;
|
||||
}
|
||||
@apply w-full relative px-2 pr-2.5;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Fragment, useEffect, useRef, useState } from "react";
|
||||
import { Link, Outlet, useLocation, useParams } from "react-router-dom";
|
||||
import { MagnifyingGlassIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||
import { MagnifyingGlassIcon, PlusIcon, CloudIcon } from "@heroicons/react/24/outline";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import GraphPanel from "./_components/tabs/GraphPanel";
|
||||
import EntitiesPanel from "./_components/tabs/EntitiesPanel";
|
||||
@@ -37,7 +37,7 @@ export default function DashboardPage() {
|
||||
const dispatch = useAppDispatch()
|
||||
const location = useLocation()
|
||||
const initialTab = location.pathname.includes("entity") ?
|
||||
1 : location.pathname.includes("market")
|
||||
0 : location.pathname.includes("market")
|
||||
? 2 : 0
|
||||
|
||||
const [tabIndex, setTabIndex] = useState<number>(initialTab)
|
||||
@@ -104,12 +104,13 @@ export default function DashboardPage() {
|
||||
onChange={setTabIndex}
|
||||
className={styles["dashboard-tabs"]}
|
||||
>
|
||||
<Tab.List className={styles["tabs-list"]}>
|
||||
<Tab.List className={`${styles["tabs-list"]}`}>
|
||||
<Tab as={Fragment}>
|
||||
{({ selected }) => (
|
||||
<Link
|
||||
to='graph'
|
||||
className={`${styles["tab"]} ${styles["graph-tab"]} ${styles[`tab-${selected}`]}`}
|
||||
aria-selected={selected}
|
||||
>
|
||||
Graphs
|
||||
</Link>
|
||||
@@ -120,8 +121,11 @@ export default function DashboardPage() {
|
||||
<Link
|
||||
to='entity'
|
||||
className={`${styles["tab"]} ${styles["entities-tab"]} ${styles[`tab-${selected}`]}`}
|
||||
aria-selected={selected}
|
||||
>
|
||||
Entities
|
||||
<span>
|
||||
Entities
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
</Tab>
|
||||
@@ -130,14 +134,15 @@ export default function DashboardPage() {
|
||||
<Link
|
||||
to='market'
|
||||
className={`${styles["tab"]} ${styles["market-tab"]} ${styles[`tab-${selected}`]}`}
|
||||
aria-selected={selected}
|
||||
>
|
||||
Market
|
||||
</Link>
|
||||
)}
|
||||
</Tab>
|
||||
<div className={styles["tab-slider"]}></div>
|
||||
<div className={styles["tab-slider"]} />
|
||||
</Tab.List>
|
||||
<section className="h-full overflow-y-scroll mb-20">
|
||||
<div className="h-full overflow-y-scroll ">
|
||||
<Tab.Panel className={styles["tab-panel"]}>
|
||||
<GraphPanel
|
||||
refetchGraphs={async () => await refetchGraphs()}
|
||||
@@ -160,7 +165,7 @@ export default function DashboardPage() {
|
||||
<Tab.Panel className={styles["tab-panel"]}>
|
||||
<MarketPanel />
|
||||
</Tab.Panel>
|
||||
</section>
|
||||
</div>
|
||||
</Tab.Group>
|
||||
{tabIndex !== 2 ? (
|
||||
<button
|
||||
@@ -171,13 +176,14 @@ export default function DashboardPage() {
|
||||
className='btn-primary mt-auto mb-4 mx-4 mr-6'
|
||||
>
|
||||
Create {tabIndex === 0 ? 'graph' : 'entity'}
|
||||
<PlusIcon />
|
||||
<PlusIcon className="!ml-7" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className='btn-primary mx-4 mr-6'
|
||||
>Add transforms
|
||||
|
||||
className='btn-primary mx-4 mr-6 mt-auto mb-4'
|
||||
>
|
||||
Connect server plugins
|
||||
<CloudIcon />
|
||||
</button>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
@@ -125,7 +125,7 @@ export default function EditEntityNode({ ctx, sendJsonMessage, closeRef }: JSONO
|
||||
/>
|
||||
);
|
||||
case 'empty':
|
||||
return <div className='hidden' />;
|
||||
return <input className='h-0 bg-transparent pointer-events-none' />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,10 +4,10 @@ import {
|
||||
TableCellsIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { useState, useEffect, useCallback, useMemo, useLayoutEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useLocation, useParams } from 'react-router-dom';
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import { Responsive, WidthProvider, Layout } from 'react-grid-layout';
|
||||
import { useGetEntitiesQuery } from '@src/app/api';
|
||||
import { useGetEntitiesQuery, useRefreshEntityPluginsQuery } from '@src/app/api';
|
||||
import { selectPositionMode, selectViewMode, setAllEdges, setAllNodes, setEditState, setNodeType, setPositionMode, setViewMode } from '@src/features/graph/graphSlice';
|
||||
import classNames from 'classnames';
|
||||
import { useAppDispatch, } from '@src/app/hooks';
|
||||
@@ -64,11 +64,11 @@ export const useResize = ({ minWidth }: UseResizeProps): UseResizeReturn => {
|
||||
export function EntityOption({ entity, onDragStart }: JSONObject) {
|
||||
return (
|
||||
<>
|
||||
<li key={entity.id} className='flex items-center w-full justify-between py-3'>
|
||||
<li key={entity.id} className='flex items-center w-full justify-between pb-2.5'>
|
||||
<div
|
||||
draggable
|
||||
onDragStart={(event) => onDragStart(event, entity.label)}
|
||||
className='flex min-w-[12rem] p-2 justify-between overflow-x-hidden from-mirage-300/10 to-mirage-300/20 bg-gradient-to-br hover:from-mirage-500/20 hover:from-40% hover:to-mirage-300/30 border-mirage-300/20 border max-h-[160px] border-l-primary-300/50 hover:border-primary-400 transition-colors duration-100 ease-out border-l-[6px] hover:border-l-[6px] rounded-md w-full'
|
||||
className='flex min-w-[12rem] p-2 justify-between overflow-x-hidden from-mirage-400/50 to-mirage-400/40 hover:from-mirage-500/40 hover:to-mirage-400/60 bg-gradient-to-br hover:from-40% border-mirage-300/20 border max-h-[160px] border-l-primary-300/50 hover:border-primary-400 transition-colors duration-100 ease-out border-l-[6px] hover:border-l-[6px] rounded-md w-full backdrop-blur-md'
|
||||
>
|
||||
<div className='flex flex-col w-full select-none'>
|
||||
<div className='flex items-start justify-between gap-x-3 w-full relative'>
|
||||
@@ -83,16 +83,15 @@ export function EntityOption({ entity, onDragStart }: JSONObject) {
|
||||
{entity.status}Installed
|
||||
</p> */}
|
||||
</div>
|
||||
<div className='mt-1 flex flex-wrap items-center gap-x-2 text-xs leading-5 text-slate-500'>
|
||||
<p className='truncate whitespace-normal leading-5 text-slate-500'>
|
||||
{' '}
|
||||
{entity.description && entity.description.length > 76 ?
|
||||
`${entity.description.slice(0, 76)}...` : entity.description}
|
||||
<div className='flex flex-wrap items-center gap-x-2 text-xs leading-5 text-slate-500'>
|
||||
<p className='truncate whitespace-normal leading-5 line-clamp-2 text-slate-500'>
|
||||
{entity.description}
|
||||
</p>
|
||||
<svg viewBox='0 0 2 2' className='h-0.5 w-0.5 fill-current'>
|
||||
<circle cx={1} cy={1} r={1} />
|
||||
</svg>
|
||||
<p className='truncate leading-5 text-slate-500 text-xs'>
|
||||
<br />
|
||||
<p className='truncate flex items-center leading-5 text-slate-500 text-xs'>
|
||||
<svg viewBox='0 0 2 2' className='h-0.5 w-0.5 fill-current mr-1.5 ml-0'>
|
||||
<circle cx={1} cy={1} r={1} />
|
||||
</svg>
|
||||
Created by {entity.author ? entity.author : 'the OSINTBuddy team'}
|
||||
</p>
|
||||
</div>
|
||||
@@ -108,19 +107,20 @@ const ResponsiveGridLayout = WidthProvider(Responsive);
|
||||
const MAX_GRAPH_LABEL_LENGTH = 22;
|
||||
|
||||
export default function EntityOptions({ positionMode, activeGraph, setElkLayout, toggleForceLayout, fitView }: JSONObject) {
|
||||
const { hid = "" } = useParams()
|
||||
const {
|
||||
data: entitiesData = { entities: [], count: 0, favorite_entities: [], favorite_count: 0 },
|
||||
data: entitiesData = { plugins: [], },
|
||||
isLoading,
|
||||
isSuccess,
|
||||
isError
|
||||
} = useGetEntitiesQuery({ skip: 0, limit: 50 })
|
||||
} = useRefreshEntityPluginsQuery({ hid })
|
||||
|
||||
const [showEntities, setShowEntities] = useState(true);
|
||||
const [searchFilter, setSearchFilter] = useState('');
|
||||
|
||||
const entities = useMemo(() => searchFilter
|
||||
? [...entitiesData?.entities.filter((entity: JSONObject) => entity.label.toLowerCase().includes(searchFilter.toLowerCase())),
|
||||
...entitiesData?.favorite_entities.filter((entity: JSONObject) => entity.label.toLowerCase().includes(searchFilter.toLowerCase()))]
|
||||
: [...entitiesData?.entities, ...entitiesData?.favorite_entities], [searchFilter, entitiesData])
|
||||
? [...entitiesData?.plugins.filter((entity: JSONObject) => entity.label.toLowerCase().includes(searchFilter.toLowerCase()))]
|
||||
: [...entitiesData?.plugins], [searchFilter, entitiesData])
|
||||
|
||||
const onDragStart = (event: DragEvent, nodeType: string) => {
|
||||
if (event?.dataTransfer) {
|
||||
@@ -195,7 +195,7 @@ export default function EntityOptions({ positionMode, activeGraph, setElkLayout,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className=' overflow-hidden rounded-md z-10 border border-mirage-400/70 from-mirage-700/95 to-mirage-500/95 bg-gradient-to-br flex flex-col h-min'
|
||||
className=' overflow-hidden rounded-md z-10 border border-mirage-400/60 from-mirage-500/30 to-mirage-500/60 bg-gradient-to-br flex flex-col h-min'
|
||||
key='entities'
|
||||
id='node-options-tour'
|
||||
>
|
||||
@@ -228,7 +228,7 @@ export default function EntityOptions({ positionMode, activeGraph, setElkLayout,
|
||||
</ol>
|
||||
{showEntities && (
|
||||
<>
|
||||
<div className='mt-2.5 hover:border-mirage-200/40 transition-colors duration-200 ease-in-out block justify-between items-center to-mirage-400/50 from-mirage-300/20 bg-gradient-to-br rounded border mb-2 mx-4 focus-within:!border-primary/40 px-3.5 py-1 text-slate-100 shadow-sm border-mirage-400/20 ring-light-900/10 focus-within:from-mirage-400/20 focus-within:to-mirage-400/30 focus-within:bg-gradient-to-l'>
|
||||
<div className='mt-2.5 hover:border-mirage-200/40 transition-colors duration-200 ease-in-out block justify-between items-center to-mirage-400/70 from-mirage-300/60 bg-gradient-to-br rounded border mb-2 mx-4 focus-within:!border-primary/40 px-3.5 py-1 text-slate-100 shadow-sm border-mirage-400/20 ring-light-900/10 focus-within:from-mirage-400/20 focus-within:to-mirage-400/30 focus-within:bg-gradient-to-l'>
|
||||
<input
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
className='block w-full placeholder:text-slate-700 bg-transparent outline-none sm:text-sm'
|
||||
@@ -245,7 +245,7 @@ export default function EntityOptions({ positionMode, activeGraph, setElkLayout,
|
||||
</div>
|
||||
<div
|
||||
key='positions'
|
||||
className=' overflow-hidden rounded-md z-10 border border-mirage-400/70 from-mirage-700/95 to-mirage-500/95 bg-gradient-to-br flex flex-col h-min'>
|
||||
className=' overflow-hidden rounded-md z-10 border border-mirage-400/60 from-mirage-500/30 to-mirage-500/60 bg-gradient-to-br flex flex-col h-min'>
|
||||
<ol className='text-sm flex select-none relative px-4 pt-1'>
|
||||
<li className='flex mr-auto'>
|
||||
<h5
|
||||
@@ -284,7 +284,7 @@ export default function EntityOptions({ positionMode, activeGraph, setElkLayout,
|
||||
type='button'
|
||||
className={classNames(
|
||||
'justify-center flex-grow rounded-l-md from-mirage-300/10 to-mirage-300/20 bg-gradient-to-br hover:from-mirage-500/20 hover:from-40% hover:to-mirage-300/30 border-mirage-300/20 relative py-3 inline-flex items-center border transition-colors duration-100 ease-in-out hover:border-primary-400/50 outline-none px-3 text-slate-500 hover:text-primary-300/80 focus:bg-mirage-800 hover:bg-mirage-600 focus:z-10',
|
||||
positionMode === 'manual' && 'bg-mirage-800 hover:bg-mirage-800 border-primary-400/50 hover:border-primary-400/50 '
|
||||
positionMode === 'manual' && 'bg-mirage-800/80 hover:bg-mirage-800 border-primary-400/50 hover:border-primary-400/50 '
|
||||
)}
|
||||
>
|
||||
<HandRaisedIcon
|
||||
@@ -301,7 +301,7 @@ export default function EntityOptions({ positionMode, activeGraph, setElkLayout,
|
||||
type='button'
|
||||
className={classNames(
|
||||
'justify-center flex-grow from-mirage-300/10 to-mirage-300/20 bg-gradient-to-br hover:from-mirage-500/20 hover:from-40% hover:to-mirage-300/30 border-mirage-300/20 relative py-3 inline-flex items-center border transition-colors duration-100 ease-in-out hover:border-primary-400/50 outline-none px-3 text-slate-500 hover:text-primary-300/80 focus:bg-mirage-800 hover:bg-mirage-600 focus:z-10',
|
||||
positionMode === 'force' && 'bg-mirage-800 hover:bg-mirage-800 border-primary-400/50 hover:border-primary-400/50 '
|
||||
positionMode === 'force' && 'bg-mirage-800/80 hover:bg-mirage-800 border-primary-400/50 hover:border-primary-400/50 '
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
@@ -326,7 +326,7 @@ export default function EntityOptions({ positionMode, activeGraph, setElkLayout,
|
||||
type='button'
|
||||
className={classNames(
|
||||
'justify-center flex-grow from-mirage-300/10 to-mirage-300/20 bg-gradient-to-br hover:from-mirage-500/20 hover:from-40% hover:to-mirage-300/30 border-mirage-300/20 relative py-3 inline-flex items-center border transition-colors duration-100 ease-in-out hover:border-primary-400/50 outline-none px-3 text-slate-500 hover:text-primary-300/80 focus:bg-mirage-800 hover:bg-mirage-600 focus:z-10',
|
||||
positionMode === 'right tree' && 'bg-mirage-800 hover:bg-mirage-800 border-primary-400/50 hover:border-primary-400/50 '
|
||||
positionMode === 'right tree' && 'bg-mirage-800/80 hover:bg-mirage-800 border-primary-400/50 hover:border-primary-400/50 '
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
@@ -348,7 +348,7 @@ export default function EntityOptions({ positionMode, activeGraph, setElkLayout,
|
||||
type='button'
|
||||
className={classNames(
|
||||
'justify-center rounded-r-md flex-grow from-mirage-300/10 to-mirage-300/20 bg-gradient-to-br hover:from-mirage-500/20 hover:from-40% hover:to-mirage-300/30 border-mirage-300/20 relative py-3 inline-flex items-center border transition-colors duration-100 ease-in-out hover:border-primary-400/50 outline-none px-3 text-slate-500 hover:text-primary-300/80 focus:bg-mirage-800 hover:bg-mirage-600 focus:z-10',
|
||||
positionMode === 'tree' && 'bg-mirage-800 hover:bg-mirage-800 border-primary-400/50 hover:border-primary-400/50 '
|
||||
positionMode === 'tree' && 'bg-mirage-800/80 hover:bg-mirage-800 border-primary-400/50 hover:border-primary-400/50 '
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ChevronUpDownIcon, PaperClipIcon, XMarkIcon } from '@heroicons/react/24
|
||||
import { Combobox } from '@headlessui/react';
|
||||
import classNames from 'classnames';
|
||||
import { ChangeEvent, Dispatch, Fragment, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Handle, Position } from 'reactflow';
|
||||
import { Handle, HandleType, Position } from 'reactflow';
|
||||
import { GripIcon, Icon } from '@src/components/Icons';
|
||||
import { Dialog } from '@headlessui/react';
|
||||
import { useAppDispatch, useAppSelector, useComponentVisible } from '@src/app/hooks';
|
||||
@@ -19,20 +19,6 @@ import {
|
||||
} from '@src/features/graph/graphSlice';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
var dropdownKey = 0;
|
||||
|
||||
const getKey = () => {
|
||||
dropdownKey += 1;
|
||||
return `k_${dropdownKey}`;
|
||||
};
|
||||
|
||||
var nodeKey = 0;
|
||||
|
||||
const getNodeKey = () => {
|
||||
nodeKey += 1;
|
||||
return `k_${nodeKey}`;
|
||||
};
|
||||
|
||||
const handleStyle = { borderColor: '#39477899', background: '#12172720', width: 12, margin: -1, height: 12 };
|
||||
|
||||
type NodeElement = NodeInput & {
|
||||
@@ -43,13 +29,10 @@ type NodeElement = NodeInput & {
|
||||
};
|
||||
|
||||
|
||||
export default function ViewEntityNode({
|
||||
ctx,
|
||||
dispatch
|
||||
}: JSONObject) {
|
||||
export default function ViewEntityNode({ ctx }: JSONObject) {
|
||||
const node = ctx.data;
|
||||
|
||||
const displayValue = useMemo(() => Array.isArray(node.elements[0]) ? node.elements[0][0]?.value : node.elements[0]?.value, [node.elements])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Handle position={Position.Right} id='r1' key='r1' type='source' style={handleStyle} />
|
||||
@@ -67,7 +50,7 @@ export default function ViewEntityNode({
|
||||
style={{ backgroundColor: node?.color?.length === 7 ? `${node.color}99` : node?.color }}
|
||||
className='header !rounded-full !p-3'
|
||||
>
|
||||
<Icon icon={node.icon} className='!h-20 !w-20 cursor-grab focus:cursor-grabbing' />
|
||||
<Icon icon={node.icon} className='!h-14 !w-14 text-slate-300/95 cursor-grab focus:cursor-grabbing' />
|
||||
</div>
|
||||
<h2 className={`absolute -left-28 -right-28 max-w-xl pointer-events-none text-center text-slate-500 text-lg top-full -bottom-10 h-auto`}>
|
||||
{displayValue?.length >= 90 ? `${displayValue.slice(0, 90)}...` : displayValue}
|
||||
@@ -7,13 +7,19 @@ import ReactFlow, {
|
||||
NodeDragHandler,
|
||||
Connection,
|
||||
Node,
|
||||
getViewportForBounds,
|
||||
MiniMap,
|
||||
ReactFlowInstance,
|
||||
getNodesBounds,
|
||||
getBoundsOfRects,
|
||||
getTransformForBounds,
|
||||
} from 'reactflow';
|
||||
import EditEntityNode from '../_components/EditEntityNode';
|
||||
import { addNodeUpdate, createEdge, disableEntityEdit, enableEntityEdit, onEdgesChange, selectEditState, setEditLabel, setEditState, updateEdgeEvent, updateNodeFlow } from '@src/features/graph/graphSlice';
|
||||
import { useAppDispatch, useAppSelector } from '@src/app/hooks';
|
||||
import EditEntityNode from './EntityEditNode';
|
||||
import { addNodeUpdate, createEdge, disableEntityEdit, enableEntityEdit, graph, onEdgesChange, selectEditState, setEditLabel, setEditState, updateEdgeEvent, updateNodeFlow } from '@src/features/graph/graphSlice';
|
||||
import { useAppDispatch, useAppSelector, useEffectOnce } from '@src/app/hooks';
|
||||
import { toast } from 'react-toastify';
|
||||
import ViewEntityNode from '../_components/ViewEntityNode';
|
||||
import { CreateEntityOnDropApiResponse, useCreateEntityOnDropMutation, useRefreshPluginsQuery } from '@src/app/api';
|
||||
import ViewEntityNode from './EntityViewNode';
|
||||
import { CreateEntityOnDropApiResponse, useCreateEntityOnDropMutation, useRefreshEntityPluginsQuery } from '@src/app/api';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import NewConnectionLine from './ConnectionLine';
|
||||
import SimpleFloatingEdge from './SimpleFloatingEdge';
|
||||
@@ -25,6 +31,7 @@ const viewOptions: FitViewOptions = {
|
||||
|
||||
// im lazy so im extending the generic JSONObject for now, feel free to fix...
|
||||
interface ProjectGraphProps extends JSONObject {
|
||||
graphInstance?: ReactFlowInstance
|
||||
}
|
||||
|
||||
export default function Graph({
|
||||
@@ -48,7 +55,7 @@ export default function Graph({
|
||||
[]
|
||||
);
|
||||
const { hid } = useParams()
|
||||
useRefreshPluginsQuery({ hid: hid as string })
|
||||
useRefreshEntityPluginsQuery({ hid: hid as string })
|
||||
|
||||
const onDragOver: DragEventHandler<HTMLDivElement> = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
@@ -68,7 +75,7 @@ export default function Graph({
|
||||
if (typeof label === 'undefined' || !label) return;
|
||||
|
||||
const graphBounds = graphRef.current.getBoundingClientRect();
|
||||
const position = graphInstance.project({
|
||||
const position = graphInstance?.project({
|
||||
x: event.clientX - graphBounds.left,
|
||||
y: event.clientY - graphBounds.top,
|
||||
});
|
||||
@@ -114,7 +121,7 @@ export default function Graph({
|
||||
[]
|
||||
);
|
||||
|
||||
const doubleClickThreshold = 325;
|
||||
const doubleClickThreshold = 320;
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [isDoubleClick, setIsDoubleClick] = useState(false)
|
||||
const [entityPosition, setEntityPosition] = useState<any>({ x: null, y: null })
|
||||
@@ -130,9 +137,31 @@ export default function Graph({
|
||||
}
|
||||
};
|
||||
|
||||
const handleGraphRead = (readType: string = 'read') => {
|
||||
const viewport: any = graphInstance?.getViewport()
|
||||
if (viewport) {
|
||||
sendJsonMessage({
|
||||
action: `${readType}:graph`,
|
||||
viewport
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: implement loading/unloading nodes after drag
|
||||
// !isDragging && handleGraphRead()
|
||||
}, [isDragging])
|
||||
|
||||
useEffect(() => {
|
||||
graphInstance?.setViewport({ x: 0, y: 0, zoom: 0.22 })
|
||||
handleGraphRead('initial_read');
|
||||
}, [graphInstance?.getViewport])
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
minZoom={0.2}
|
||||
onlyRenderVisibleElements={true}
|
||||
nodeDragThreshold={20}
|
||||
minZoom={0.22}
|
||||
maxZoom={2.0}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
@@ -154,8 +183,12 @@ export default function Graph({
|
||||
setClickDelta(newDelta)
|
||||
setIsDoubleClick(isDouble)
|
||||
}}
|
||||
onMoveStart={() => !isDragging && setIsDragging(true)}
|
||||
onMoveEnd={() => setIsDragging(false)}
|
||||
fitViewOptions={viewOptions}
|
||||
nodeTypes={nodeTypes}
|
||||
onDragStart={() => setIsDragging(true)}
|
||||
onDragEnd={() => setIsDragging(false)}
|
||||
panActivationKeyCode='Space'
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
onPaneClick={onPaneClick}
|
||||
@@ -164,9 +197,8 @@ export default function Graph({
|
||||
onSelectionContextMenu={onMultiSelectionCtxMenu}
|
||||
connectionLineComponent={NewConnectionLine}
|
||||
elevateNodesOnSelect={true}
|
||||
fitView
|
||||
>
|
||||
<Background size={2} variant={BackgroundVariant.Dots} className='bg-transparent' color='#35426FAA' />
|
||||
<Background size={2.5} variant={BackgroundVariant.Dots} className='bg-transparent' color='#334155e6' />
|
||||
</ReactFlow>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useStore, getBezierPath, BaseEdge, EdgeLabelRenderer } from 'reactflow';
|
||||
|
||||
import { getEdgeParams } from './utils';
|
||||
import classNames from 'classnames';
|
||||
import { ChevronDoubleDownIcon, ChevronDownIcon } from '@heroicons/react/20/solid';
|
||||
|
||||
function EdgeLabel({ transform, label }: { transform: string; label: string }) {
|
||||
return (
|
||||
<>
|
||||
{label && (
|
||||
<div
|
||||
style={{ transform }}
|
||||
className="absolute border-2 bg-slate-800/5 pointer-events-auto cursor-grab font-semibold border-slate-700 p-1 px-3 rounded-md text-[0.6rem] leading-none capitalize text-slate-500 hover:text-slate-400/80 backdrop-blur-md flex items-center justify-between font-display"
|
||||
>
|
||||
<p className="flex items-center justify-between cursor-pointer">
|
||||
{label} <ChevronDownIcon className='h-4 w-4 ml-2 origin-center hover:rotate-3' />
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SimpleFloatingEdge({ id, source, target, markerEnd, style, label }: JSONObject) {
|
||||
const sourceNode = useStore(useCallback((store) => store.nodeInternals.get(source), [source]));
|
||||
const targetNode = useStore(useCallback((store) => store.nodeInternals.get(target), [target]));
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode);
|
||||
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX: sx,
|
||||
sourceY: sy,
|
||||
sourcePosition: sourcePos,
|
||||
targetPosition: targetPos,
|
||||
targetX: tx,
|
||||
targetY: ty,
|
||||
});
|
||||
console.log(style)
|
||||
return (
|
||||
<>
|
||||
<BaseEdge
|
||||
id={id}
|
||||
path={edgePath}
|
||||
// strokeWidth={4}
|
||||
markerEnd={markerEnd}
|
||||
style={{ ...style, cursor: 'grab' }} />
|
||||
|
||||
<EdgeLabelRenderer>
|
||||
<EdgeLabel label={label?.replace("_", " ")} transform={`translate(-50%, -50%) translate(${labelX}px,${labelY}px)`} />
|
||||
{/* <div
|
||||
style={{
|
||||
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||
borderRadius: 5,
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
}}
|
||||
className="nodrag nopan absolute text-slate-400/95 p-1"
|
||||
>
|
||||
{label}
|
||||
</div> */}
|
||||
</EdgeLabelRenderer>
|
||||
{/* <path */}
|
||||
{/* /> */}
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default SimpleFloatingEdge;
|
||||
@@ -1,37 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useStore, getBezierPath } from 'reactflow';
|
||||
|
||||
import { getEdgeParams } from './utils';
|
||||
|
||||
function SimpleFloatingEdge({ id, source, target, markerEnd, style }: JSONObject) {
|
||||
const sourceNode = useStore(useCallback((store) => store.nodeInternals.get(source), [source]));
|
||||
const targetNode = useStore(useCallback((store) => store.nodeInternals.get(target), [target]));
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode);
|
||||
|
||||
const [edgePath] = getBezierPath({
|
||||
sourceX: sx,
|
||||
sourceY: sy,
|
||||
sourcePosition: sourcePos,
|
||||
targetPosition: targetPos,
|
||||
targetX: tx,
|
||||
targetY: ty,
|
||||
});
|
||||
|
||||
return (
|
||||
<path
|
||||
id={id}
|
||||
className="react-flow__edge-path"
|
||||
d={edgePath}
|
||||
strokeWidth={5}
|
||||
markerEnd={markerEnd}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default SimpleFloatingEdge;
|
||||