Compare commits

4 Commits

Author SHA1 Message Date
jerlendds
1320fe4916 update readme with notice 2025-04-13 08:15:12 -06:00
Nivalis ❆
9f25d38f9b Merge pull request #66 from bastian-thiede/fix/greedy-docker-volume-cleanup
[Bug] Cleanup routine for volumes is presumably too greedy
2024-05-06 14:16:30 -06:00
Bastian Thiede
4eb60fa019 Modify the method to delete volumes to consider compose configuration 2024-05-04 19:40:46 +02:00
jerlendds
622fc581f9 Add notice to README 2024-03-04 10:17:37 -07:00
110 changed files with 2590 additions and 2616 deletions

View File

@@ -1,46 +0,0 @@
---
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/)**

View File

@@ -1,7 +0,0 @@
---
title: Entities API reference
pageTitle: Entities API reference
description:
---
Coming soon. ETA 12-24 weeks from June 10th 2023.

View File

@@ -1,7 +0,0 @@
---
title: Entities
pageTitle: Entities
description:
---
Coming soon. ETA 12-24 weeks from June 10th 2023.

View File

@@ -1,41 +0,0 @@
---
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.

View File

@@ -1,43 +0,0 @@
---
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)

View File

@@ -1,55 +0,0 @@
---
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)

View File

@@ -1,16 +0,0 @@
---
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 %}

View File

@@ -1,7 +0,0 @@
---
title: Plugin recipes
pageTitle: Plugin recipes
description:
---
Coming soon. ETA 12-24 weeks from June 10th 2023.

View File

@@ -1,88 +0,0 @@
---
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
```

View File

@@ -1,7 +0,0 @@
---
title: Plugins API reference
pageTitle: Plugins API reference
description:
---
Coming soon. ETA 12-24 weeks from June 10th 2023.

View File

@@ -1,12 +0,0 @@
---
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.
![ob-demo 2023-07-14 20-51](https://github.com/jerlendds/osintbuddy/assets/29207058/b69e08f3-ac2a-4cef-9a85-713e7df6b12f)

View File

@@ -1,8 +0,0 @@
---
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.

View File

@@ -1,7 +0,0 @@
---
title: Settings API reference
pageTitle: Settings API reference
description:
---
Coming soon. ETA 12-24 weeks from June 10th 2023.

View File

@@ -1,7 +0,0 @@
---
title: Transforms API reference
pageTitle: Transforms API reference
description:
---
Coming soon. ETA 12-24 weeks from June 10th 2023.

View File

@@ -1,6 +0,0 @@
---
title: Transforms
pageTitle: Transforms
description:
---

View File

@@ -1,26 +0,0 @@
---
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...

View File

@@ -1,72 +0,0 @@
---
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
```

View File

@@ -1,37 +0,0 @@
---
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.

View File

@@ -1,45 +0,0 @@
# 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 dont care if youre liberal or conservative, Black or white, straight or gay, or anything in between!** In fact, we wont 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.

194
README.md
View File

@@ -20,55 +20,32 @@
------- -------
|    [osintbuddy-plugins](https://github.com/jerlendds/osintbuddy-plugins)    |    [osintbuddy.com](https://osintbuddy.com)    |     [osintbuddy discord](https://discord.gg/gsbbYHA3K3)    |    [osintbuddy-core-plugins](https://github.com/jerlendds/osintbuddy-core-plugins)    | |    [plugins-system](https://github.com/osintbuddy/plugins)    |    [osintbuddy.com](https://osintbuddy.com)    |    [osintbuddy discord](https://discord.gg/b8vW4J4skv)    |    [entities](https://github.com/osintbuddy/entities)    |    [pypi package](https://pypi.org/project/osintbuddy/)    |
<span style="display: inline-block; width:830px"> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span>
## Introducing OSINTBuddy
**Notice** - *No, this project is not "dead"*, we are currently undergoing a big rewrite (Python plugins will still be supported!). The rewrite can be found at [github.com/osintbuddy/osintbuddy](https://github.com/osintbuddy/osintbuddy)
## Introducing OSINTBuddy <p>
Welcome to the OSINTBuddy project where you can connect, combine,
and get insight from unstructured and public data as results that
can be explored step-by-step. An easy-to-use plugin system allows any
Python developer to quickly integrate new data sources so you can focus
on discovering, interacting, and visualizing what's important to you
</p>
<p> 🚧 <ins>This repo of osintbuddy is archived and will recieve no further attention. Please see [osintbuddy/osintbuddy](https://github.com/osintbuddy/osintbuddy) for the version under active development</ins> 🚧
Welcome to the OSINTBuddy project where you can connect, combine,
and get insight from unstructured and public data as results that
can be explored step-by-step. An easy-to-use plugin system allows any
Python developer to quickly integrate new data sources so you can focus
on discovering, interacting, and visualizing what's important to you
</p>
🚧 <ins>Work in progress</ins> 🚧
<br/> <br/>
[2024-01-09 20-55.webm](https://github.com/jerlendds/osintbuddy/assets/29207058/fb07fa95-30af-45aa-a75b-2fa1473bc37b) [osib-demo-2025-04-12T21-13.webm](https://github.com/user-attachments/assets/4a7e21f1-1e80-42b0-a4a6-91477e53ba4c)
⚠️ **Searching for contributors**, please contact me on the **[forum (work-in-progress)](https://forum.osintbuddy.com)** ⚠️ The above demo is from the new version of osintbuddy found at [github.com/osintbuddy/osintbuddy](https://github.com/osintbuddy/osintbuddy) ⚠️
or on the **[discord](https://discord.gg/gsbbYHA3K3)** ⚠️
To get started please see [CONTRIBUTING.md](./.github/CONTRIBUTING.md), and [CONTRIBUTOR_AGREEMENT.md](./.github/CONTRIBUTOR_AGREEMENT.md) to agree with the AGPLv3
--- ---
</p> </p>
<details open="open">
<summary>
<b>Table of Contents</b>
</summary>
<ol>
<li>
<a href="#what-is-osintbuddycom">What is OSINTBuddy</a>
</li>
<li>
<a href="#getting-started">Getting Started</a>
<ul>
<li><a href="#installation">Installation</a></li>
</ul>
</li>
<li><a href="#usage">Usage</a></li>
<li><a href="#roadmap">Roadmap</a></li>
<li><a href="#contributing">Contributing</a></li>
<li><a href="#contact">Contact</a></li>
<li><a href="#sponsor-osintbuddy">Sponsor OSINTBuddy</a></li>
</ol>
</details>
### What is <a referrerpolicy="unsafe-url" target="_blank" href="https://osintbuddy.com">osintbuddy.com</a>? ### What is <a referrerpolicy="unsafe-url" target="_blank" href="https://osintbuddy.com">osintbuddy.com</a>?
@@ -95,149 +72,10 @@ OSINT is a method of working with, assessing and ranking information — We are
We've decided to do something about it. The rapid developments in technologies such as AI and big data analytics have opened new horizons for OSINT which weren't previously available. We want to put all of that information at your fingertips. We want actionable intelligence quickly and to the point, that's why we're building an open-source OSINT tool that's free. Free to use, free to modify, free to do with as you wish, and built with plain old web technologies anyone can learn. But this isn't really the project. This is a free new community. A community for OSINT enthusiasts around the world and we need your help to design it, to program it, and to build it. We want to hear your suggestions, your ideas, and we're going to build it right in front of your eyes. The notion of a “needle in a haystack” is taken to the extreme on the internet. Let's build a magnet. We've decided to do something about it. The rapid developments in technologies such as AI and big data analytics have opened new horizons for OSINT which weren't previously available. We want to put all of that information at your fingertips. We want actionable intelligence quickly and to the point, that's why we're building an open-source OSINT tool that's free. Free to use, free to modify, free to do with as you wish, and built with plain old web technologies anyone can learn. But this isn't really the project. This is a free new community. A community for OSINT enthusiasts around the world and we need your help to design it, to program it, and to build it. We want to hear your suggestions, your ideas, and we're going to build it right in front of your eyes. The notion of a “needle in a haystack” is taken to the extreme on the internet. Let's build a magnet.
### Key Alpha Features
- Visual representation and layout modes for fetched data for easy understanding and editing.
- Simplified data fetching/transformations from custom sources using Python plugins, custom plugin/entity field layouts, check out the [osintbuddy](https://pypi.org/project/osintbuddy/) PyPi package for more details and expect many more input fields like files and checkboxes/toggles to come in the future.
- A development platform that is open for contributions.
- Works most of the time :)
### The future
At the core of this project lies a far more ambitious vision than the mere creation of an all-in-one data aggregation and analysis tool. What we aspire to start is the birth of a knowledge-driven community, passionately dedicated to the development of an evolving intelligence tool - a system and platform designed for discovering, interacting, and visualizing information to derive actionable insights. We imagine a system that not only learns from and evolves with your skills but also augments them. Here are some of our ideas for the future in no particular order:
#### Intuitive search and discovery tools
- We want you to be able to interact with our algorithms, filter, segment, search for particular data, apply advanced query filters, data layouts, build queries visually, and more so you can uncover insights that matter via an easy-to-navigate web-based interface.
#### Collective intelligence through collaborative workspaces
- We want to create a platform that promotes collective intelligence. Think of the currently popular collaborative workspaces such as [AFFiNE](https://github.com/toeverything/AFFiNE), Logseq, or Obsidian but built into our data workspace. A system where each individual user contributes to the pool of knowledge, where you can share your graph environments, where you can track changes with annotations and comments, where insights from your workspaces can be optionally published and engaged with by a global community. Where every connection made, every data source integrated, every plugin created and shared by members of our community contributes to advancing insights around the world.
#### Real-time monitoring
- We want functionality that can provide real-time updates for a set of entities or scans, notifying you when theres new data, changes, and or emerging patterns in your field of interest. Automatically retrieve insights as time goes on and learn from history with snapshots of your past data.
#### History graphs
- We want to create a browser extension that allows you to seamlessly browse the web while also populating your OSINTBuddy graph. Each website you visit is mapped as an entity, with lines connecting them in the sequence youve traveled. Not just a bookmark list or a history tab, the history graph could maintain the context, showing not just where youve been, but also how and when you got there, and with the ability to extract a comment or piece of data out of a website you visit into an entity, you'll rarely have to leave your favorite web browser.
#### AI, swarm intelligence, and evolutionary algorithms
- Integrating AI, swarm intelligence, and evolutionary algorithms could let us create a tool that can continually evolve and optimize your operations over time. From NLP and sentiment analysis to search and anomaly detection with swarm intelligence, bringing data science tools and techniques to OSINTBuddy could let use create a powerful tool that reveals deep insights among a "haystack" of noisy information.
This is a project that will most likely not be finished for many years if ever, and that will require collaboration among experts in many fields. We're always looking for help, from writing documentation, researching feature ideas, designing the UX/UI, donating, to simply marketing and sharing the project, anything you contribute helps realize a vision for what could turn into a cross-disciplinary toolkit for working with information. Will you join me?
## Getting Started
To start using OSINTBuddy, follow these simple installation steps:
*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 application to work*
### Installation
1. Clone the repo and submodules
```sh
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:
```sh
# ./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.
- **URLs**
- Frontend: http://localhost:3000
- Casdoor: http://localhost:45910
- Backend: http://localhost:48997/api
- Documentation: http://localhost:48997/docs
- Access OSINTBuddy through the URLs provided for the frontend, backend, and documentation.
- Default login:
- *username:* osintbuddy
- *password:* osintbuddy
## [↑](#introducing-osintbuddy)Roadmap
~~See the [trello board](https://trello.com/b/99Q70frX/) for a list of our upcoming features.~~
See the forum: [forum.osintbuddy.com](https://forum.osintbuddy.com), for a list of our upcoming features, to contribute feedback, ideas, and more (*Note: We're currently in the process of theming and setting up the forum so we can move trello issues over. We're not sure when this will be completed. For the time being you can count on dev log updates being on the forum or on discord from now on*).
See the [open issues](https://github.com/jerlendds/osintbuddy/issues)
for a list of requested features (and known issues).
## [↑](#introducing-osintbuddy)Contributing
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. But if you'd like to make a significant change to this project or the `osintbuddy-plugins` project, please first [create an issue](https://github.com/jerlendds/osintbuddy/issues/new?assignees=jerlendds&labels=Type%3A+Suggestion&projects=&template=feature.md&title=%5BFEATURE+REQUEST%5D+Your_feature_request_here) or open a contributor post on the [forum](https://forum.osintbuddy.com/c/osintbuddy-contributors/11) to get feedback before spending too much time. We don't want you to invest your time on changes we are already working on. Also, for details on how to get up and running with the project you can check out [CONTRIBUTING.md](./.github/CONTRIBUTING.md). If you want to contribute directly please ensure you agree with the [CONTRIBUTOR_AGREEMENT.md](./.github/CONTRIBUTOR_AGREEMENT.md), in short, we want to ensure you're okay with your changes being licensed under the AGPLv3.
1. Fork the Project
2. Create your Feature Branch (`git checkout -b feature/add-core-feature`)
3. Commit your Changes (`git commit -m 'feat: add core feature to osintbuddy app'`)
4. Push to the Branch (`git push origin feature/add-core-feature`)
5. Open a Pull Request
*Note: If you're working on the `develop` branch you want to ensure the `osintbuddy-plugins` submodule is also on the develop branch.*
## [↑](#introducing-osintbuddy)License ## [↑](#introducing-osintbuddy)License
We are using the [GNU Affero General Public License v3.0](https://choosealicense.com/licenses/agpl-3.0/) [AGPL](https://choosealicense.com/licenses/agpl-3.0/)
[Why the AGPLv3?](https://ploum.net/2023-06-19-more-rms.html)
> In the early eighties, RMS realised that software was transformed from "a way to use a machine" to a product or a commodity. He foresaw that this would put an end to collective intelligence and to knowledge sharing.
> For the last 30 years, businesses and proponents of Open Source, including Linus Torvalds, have been decrying the GPL because of the essential right of "doing business" aka "privatising the common". Microsoft, through Github, Google and Apple pushed for MIT/BSD licensed software as the open source standard.
> Our freedoms were taken away by proprietary software which is mostly coded by ourselves. For free. We spent our free time developing, debugging, testing software before handing them to corporations... Without Non-copyleft Open Source, there would be no proprietary MacOS, OSX nor Android. There would be no Facebook, no Amazon. We created all the components.
### The four freedoms and one obligation of free software
- The right to use the software at your own discretion
- The right to study the software
- The right to modify the software
- The right to redistribute the software, including with modifications
- The obligation to keep those four rights, effectively keeping the software in the commons.
> We need to realize that any software without that last obligation will, sooner or later, become an oppression tool against ourselves. And that maintaining the commons is not only about software. Its about everything we are as a society and everything we are losing against individual greed.
*Note: the [OSINTBuddy PyPi package](https://github.com/jerlendds/osintbuddy-plugins) is MIT licensed. We understand some individuals and businesses may not want to share their plugins developed in-house.*
Patched `aiogremlin` library: [jerlendds/gremlinpy](https://github.com/jerlendds/gremlinpy/)
## [↑](#introducing-osintbuddy)Contact
[Open an issue](https://github.com/jerlendds/osintbuddy/issues/new?assignees=jerlendds&labels=Type%3A+Suggestion&projects=&template=feature.md&title=%5BFEATURE+REQUEST%5D+Your_feature_request_here) if you need to get in touch with me or send an email to <a href="mailto:jerlendds@osintbuddy.com">jerlendds@osintbuddy.com</a>.
## [↑](#introducing-osintbuddy)Sponsor OSINTBuddy
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)
[![Yearly OpenCollective Income](https://badgen.net/opencollective/yearly/osintbuddy)](https://opencollective.com/openinfolabs/projects/osintbuddy#category-CONTRIBUTE)
[![OpenCollective Backers](https://badgen.net/opencollective/backers/osintbuddy)](https://opencollective.com/openinfolabs/projects/osintbuddy#category-CONTRIBUTE)
[contributors-shield]: https://img.shields.io/github/contributors/jerlendds/osintbuddy.svg?style=for-the-badge [contributors-shield]: https://img.shields.io/github/contributors/jerlendds/osintbuddy.svg?style=for-the-badge

View File

@@ -1,11 +1,11 @@
from fastapi import APIRouter from fastapi import APIRouter
from app.core.config import settings from app.core.config import settings
from .endpoints import entity, graph, graph_inquiry, login, scan, account, config from .endpoints import entity, graph, node, login, scan, account, config
api_router = APIRouter(prefix=settings.API_V1_STR) api_router = APIRouter(prefix=settings.API_V1_STR)
api_router.include_router(graph_inquiry.router, tags=["Nodes"]) api_router.include_router(node.router, tags=["Nodes"])
api_router.include_router(config.router, tags=["Config"]) api_router.include_router(config.router, tags=["Config"])
api_router.include_router(login.router, tags=["Login"]) api_router.include_router(login.router, tags=["Login"])
api_router.include_router(account.router, tags=["Accounts"]) api_router.include_router(account.router, tags=["Accounts"])

View File

@@ -14,12 +14,12 @@ router = APIRouter(prefix="/account")
@router.get( @router.get(
"/", "/",
response_model=Union[schemas.UserInDBBase, schemas.HTTPError], response_model=Union[schemas.User, schemas.HTTPError],
response_model_exclude_none=True response_model_exclude_none=True
) )
async def get_account( async def get_account(
request: APIRequest, request: APIRequest,
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)], user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
) -> models.User: ) -> models.User:
try: try:
return user return user

View File

@@ -15,7 +15,7 @@ router = APIRouter(prefix="/config")
@router.get("/casdoor", response_model=Any) @router.get("/casdoor", response_model=Any)
async def get_casdoor_config( async def get_casdoor_config(
request: APIRequest, request: APIRequest,
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)] user: Annotated[schemas.User, Depends(deps.get_user_from_session)]
): ):
sdk = request.app.state.CASDOOR_SDK sdk = request.app.state.CASDOOR_SDK
casdoor_apps = sdk.get_applications() casdoor_apps = sdk.get_applications()

View File

@@ -1,10 +1,8 @@
import os, importlib, inspect
from uuid import UUID from uuid import UUID
from typing import Annotated from typing import Annotated
from app.schemas.entities import ENTITY_NAMESPACE
from app.schemas.entities import ENTITY_NAMESPACE, Entity from osintbuddy import Registry
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.templates.default import plugin_source_template
from osintbuddy.utils.generic import to_snake_case from osintbuddy.utils.generic import to_snake_case
from fastapi import APIRouter, HTTPException, Depends from fastapi import APIRouter, HTTPException, Depends
@@ -12,6 +10,7 @@ from sqlalchemy.orm import Session
from starlette import status from starlette import status
from app.api import deps from app.api import deps
from app.api.api_v1.endpoints.node import refresh_local_entities
from app import crud, schemas from app import crud, schemas
from app.core.logger import get_logger from app.core.logger import get_logger
@@ -20,14 +19,15 @@ router = APIRouter(prefix="/entity")
async def fetch_node_transforms(plugin_label): async def fetch_node_transforms(plugin_label):
plugin = await EntityRegistry.get_plugin(plugin_label=to_snake_case(plugin_label)) plugin = await Registry.get_plugin(plugin_label=to_snake_case(plugin_label))
if plugin is not None: if plugin is not None:
return plugin().transform_labels labels = plugin().transform_labels
return labels
@router.get("/plugins/transform/") @router.get("/plugins/transform/")
async def get_entity_transforms( async def get_entity_transforms(
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)], user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
label: str label: str
): ):
try: try:
@@ -41,8 +41,8 @@ async def get_entity_transforms(
detail="Invalid transform error. Please file an issue if this occurs." detail="Invalid transform error. Please file an issue if this occurs."
) )
except Exception as e: except Exception as e:
log.error(e)
log.error("Error inside entity.get_entity_transforms") log.error("Error inside entity.get_entity_transforms")
log.error(e)
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="There was an error fetching transforms, please try again." detail="There was an error fetching transforms, please try again."
@@ -51,12 +51,13 @@ async def get_entity_transforms(
@router.get("/details/{hid}", response_model=schemas.Entity) @router.get("/details/{hid}", response_model=schemas.Entity)
async def get_entity( async def get_entity(
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)], user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
hid: Annotated[int, Depends(deps.get_entity_id)], hid: Annotated[str, Depends(deps.get_entity_id)],
db: Annotated[Session, Depends(deps.get_db)], db: Annotated[Session, Depends(deps.get_db)],
): ):
try: try:
return crud.entities.get(db=db, id=hid) entities = crud.entities.get(db=db, id=hid)
return entities
except Exception as e: except Exception as e:
log.error('Error inside entity.get_entity:') log.error('Error inside entity.get_entity:')
log.error(e) log.error(e)
@@ -71,7 +72,7 @@ async def get_entity(
response_model=schemas.AllEntitiesList response_model=schemas.AllEntitiesList
) )
async def get_entities( async def get_entities(
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)], user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
db: Annotated[Session, Depends(deps.get_db)], db: Annotated[Session, Depends(deps.get_db)],
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100,
@@ -79,22 +80,6 @@ async def get_entities(
# try: # try:
if limit > 50: if limit > 50:
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_entities, entities_count = crud.entities.get_many_by_favorites(
db=db, db=db,
skip=skip, skip=skip,
@@ -109,17 +94,16 @@ async def get_entities(
) )
entities = [] entities = []
favorite_entities = []
for entity in db_entities: for entity in db_entities:
entity = entity._asdict() entity = entity._asdict()
entity["id"] = deps.get_hid(db_id=entity.get("id"), ns=ENTITY_NAMESPACE) entity["id"] = deps.hid(db_id=entity.get("id"), ns=ENTITY_NAMESPACE)
entities.append(entity) entities.append(entity)
favorite_entities = []
for entity in db_favorite_entities: for entity in db_favorite_entities:
entity = entity._asdict() entity = entity._asdict()
entity["id"] = deps.get_hid(db_id=entity.get("id"), ns=ENTITY_NAMESPACE) entity["id"] = deps.hid(db_id=entity.get("id"), ns=ENTITY_NAMESPACE)
favorite_entities.append(entity) favorite_entities.append(entity)
return { return {
"entities": entities, "entities": entities,
"count": entities_count, "count": entities_count,
@@ -136,12 +120,12 @@ async def get_entities(
@router.post("") @router.post("")
async def create_entity( async def create_entity(
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)], user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
entity: schemas.PostEntityCreate, entity: schemas.PostEntityCreate,
db: Annotated[Session, Depends(deps.get_db)], db: Annotated[Session, Depends(deps.get_db)],
): ):
try: try:
obj_in = schemas.EntityCreate( return crud.entities.create(db, obj_in=schemas.EntityCreate(
label=entity.label, label=entity.label,
author=entity.author, author=entity.author,
description=entity.description, description=entity.description,
@@ -150,8 +134,7 @@ async def create_entity(
description=entity.description, description=entity.description,
author=entity.author author=entity.author
) )
) ))
return crud.entities.create(db, obj_in)
except Exception as e: except Exception as e:
log.error('Error inside entity.create_entity:') log.error('Error inside entity.create_entity:')
log.error(e) log.error(e)
@@ -163,17 +146,19 @@ async def create_entity(
@router.put("/{hid}") @router.put("/{hid}")
async def update_entity_by_id( async def update_entity_by_id(
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)], user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
hid: Annotated[str, Depends(deps.get_entity_id)], hid: Annotated[str, Depends(deps.get_entity_id)],
obj_in: schemas.EntityUpdate, obj_in: schemas.EntityBase,
db: Annotated[Session, Depends(deps.get_db)], db: Annotated[Session, Depends(deps.get_db)],
): ):
try: try:
db_obj = crud.entities.get(db=db, id=hid) db_obj = crud.entities.get(db=db, id=hid)
entity = crud.entities.update(db=db, db_obj=db_obj, obj_in=obj_in) entity = crud.entities.update(db=db, db_obj=db_obj, obj_in=obj_in)
refresh_local_entities(db)
return entity return entity
except Exception as e: except Exception as e:
log.error('Error inside entity.update_entity_by_id:') log.error('Error inside entity.update_entity_by_uuid:')
log.error(e) log.error(e)
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
@@ -183,7 +168,7 @@ async def update_entity_by_id(
@router.delete("/{hid}") @router.delete("/{hid}")
async def delete_entity( async def delete_entity(
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)], user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
hid: Annotated[str, Depends(deps.get_entity_id)], hid: Annotated[str, Depends(deps.get_entity_id)],
db: Annotated[Session, Depends(deps.get_db)], db: Annotated[Session, Depends(deps.get_db)],
): ):
@@ -200,7 +185,7 @@ async def delete_entity(
@router.put("/{hid}/favorite") @router.put("/{hid}/favorite")
async def update_entity_favorite_id( async def update_entity_favorite_id(
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)], user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
hid: Annotated[str, Depends(deps.get_entity_id)], hid: Annotated[str, Depends(deps.get_entity_id)],
db: Annotated[Session, Depends(deps.get_db)], db: Annotated[Session, Depends(deps.get_db)],
): ):

View File

@@ -8,7 +8,7 @@ from app.api import deps
from app import crud, schemas from app import crud, schemas
from app.core.logger import get_logger from app.core.logger import get_logger
from app.db.janus import ProjectGraphConnection, janus_create_db from app.db.janus import ProjectGraphConnection, janus_create_db
from app.api.utils import get_hid from app.api.utils import hid
log = get_logger("api_v1.endpoints.graph") log = get_logger("api_v1.endpoints.graph")
router = APIRouter(prefix="/graph") router = APIRouter(prefix="/graph")
@@ -16,7 +16,7 @@ router = APIRouter(prefix="/graph")
@router.get("/{hid}", response_model=schemas.Graph) @router.get("/{hid}", response_model=schemas.Graph)
async def get_graph( async def get_graph(
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)], user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
hid: Annotated[str, Depends(deps.get_graph_id)], hid: Annotated[str, Depends(deps.get_graph_id)],
db: Annotated[Session, Depends(deps.get_db)], db: Annotated[Session, Depends(deps.get_db)],
): ):
@@ -37,7 +37,7 @@ async def get_graph(
@router.patch('/{hid}/favorite/') @router.patch('/{hid}/favorite/')
async def update_graph_favorite_id( async def update_graph_favorite_id(
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)], user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
hid: Annotated[str, Depends(deps.get_graph_id)], hid: Annotated[str, Depends(deps.get_graph_id)],
db: Annotated[Session, Depends(deps.get_db)], db: Annotated[Session, Depends(deps.get_db)],
): ):
@@ -66,7 +66,7 @@ async def update_graph_favorite_id(
response_model=schemas.AllGraphsList, response_model=schemas.AllGraphsList,
) )
async def get_graphs( async def get_graphs(
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)], user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
db: Annotated[Session, Depends(deps.get_db)], db: Annotated[Session, Depends(deps.get_db)],
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100,
@@ -112,7 +112,7 @@ async def get_graphs(
response_model=schemas.GraphsList response_model=schemas.GraphsList
) )
async def get_graphs_by_favorite( async def get_graphs_by_favorite(
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)], user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
db: Annotated[Session, Depends(deps.get_db)], db: Annotated[Session, Depends(deps.get_db)],
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100,
@@ -139,7 +139,7 @@ async def get_graphs_by_favorite(
response_model=schemas.Graph response_model=schemas.Graph
) )
async def create_graph( async def create_graph(
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)], user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
obj_in: schemas.GraphCreate, obj_in: schemas.GraphCreate,
db: Annotated[Session, Depends(deps.get_db)], db: Annotated[Session, Depends(deps.get_db)],
): ):
@@ -162,7 +162,7 @@ async def create_graph(
@router.delete('') @router.delete('')
async def delete_graph( async def delete_graph(
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)], user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
hid: Annotated[str, Depends(deps.get_graph_id)], hid: Annotated[str, Depends(deps.get_graph_id)],
db: Annotated[Session, Depends(deps.get_db)], db: Annotated[Session, Depends(deps.get_db)],
): ):
@@ -180,7 +180,7 @@ async def delete_graph(
@router.get('/{hid}/stats') @router.get('/{hid}/stats')
async def get_graph_stats( async def get_graph_stats(
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)], user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
hid: Annotated[str, Depends(deps.get_graph_id)], hid: Annotated[str, Depends(deps.get_graph_id)],
db: Annotated[Session, Depends(deps.get_db)], db: Annotated[Session, Depends(deps.get_db)],
): ):

View File

@@ -1,14 +1,12 @@
from uuid import UUID from uuid import UUID
from typing import List, Callable, Tuple, Any, Annotated, Dict from typing import List, Callable, Tuple, Any, Annotated
from fastapi import APIRouter, WebSocket, WebSocketException, WebSocketDisconnect, HTTPException, Depends from fastapi import APIRouter, WebSocket, WebSocketException, WebSocketDisconnect, HTTPException, Depends
from sqlalchemy import or_
from websockets.exceptions import ConnectionClosedError from websockets.exceptions import ConnectionClosedError
from gremlinpy.process.graph_traversal import AsyncGraphTraversal from gremlinpy.process.graph_traversal import AsyncGraphTraversal
from gremlin_python.process.graph_traversal import __ as _g 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 gremlin_python.process.traversal import T, Cardinality
from osintbuddy.utils import to_snake_case, MAP_KEY, chunks from osintbuddy.utils import to_snake_case, MAP_KEY, chunks
from osintbuddy import EntityRegistry, TransformUse, EntityConfig from osintbuddy import Registry, PluginUse, load_plugins
from osintbuddy.errors import OBPluginError from osintbuddy.errors import OBPluginError
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from starlette import status from starlette import status
@@ -22,19 +20,30 @@ from app.core.config import settings
log = get_logger("api_v1.endpoints.nodes") log = get_logger("api_v1.endpoints.nodes")
router = APIRouter(prefix="/node") 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): async def fetch_node_transforms(plugin_label):
plugin = await EntityRegistry.get_plugin(plugin_label=plugin_label) plugin = await Registry.get_plugin(plugin_label=plugin_label)
if plugin is not None: if plugin is not None:
labels = plugin().transform_labels labels = plugin().transform_labels
return labels return labels
def add_node_element(vertex, element: dict or List[dict], data_labels: List[str]): def add_node_element(vertex, element: dict or List[dict], data_labels: List[str]):
# Remove stylistic values unrelated to element data # Remove stylistic values unrelated to element data
# Some osintbuddy.elements of type displays dont have an icon or options # Some osintbuddy.elements of type displays dont have an icon or options
icon = element.pop('icon', None) icon = element.pop('icon', None)
options = element.pop('options', None) options = element.pop('options', None)
label = element.pop('label') label = element.pop('label')
elm_type = element.pop('type') elm_type = element.pop('type')
@@ -47,6 +56,7 @@ 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) 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 # Save the data labels so we can assign these as meta properties later
data_labels.append(to_snake_case(label)) data_labels.append(to_snake_case(label))
element['type'] = elm_type element['type'] = elm_type
element['icon'] = icon element['icon'] = icon
element['label'] = label element['label'] = label
@@ -107,11 +117,7 @@ async def save_node_on_drop(
} }
} }
async def load_nodes_from_db(uuid: UUID, viewport=None) -> tuple[list, list]: async def load_initial_graph(uuid: UUID) -> tuple[list, list]:
zoom_scale = viewport.get('zoom')
# x = viewport.get('x')
# y = viewport.get('y')
async with ProjectGraphConnection(uuid) as graph: async with ProjectGraphConnection(uuid) as graph:
edges: list = await graph.V().outE().project('from', 'edge', 'to')\ edges: list = await graph.V().outE().project('from', 'edge', 'to')\
.by(_g.outV()).by(_g.valueMap(True)).by(_g.inV()).union( .by(_g.outV()).by(_g.valueMap(True)).by(_g.inV()).union(
@@ -119,20 +125,8 @@ async def load_nodes_from_db(uuid: UUID, viewport=None) -> tuple[list, list]:
_g.project('from').by(_g.select('from')).unfold(), _g.project('from').by(_g.select('from')).unfold(),
_g.project('to').by(_g.select('to')).unfold() _g.project('to').by(_g.select('to')).unfold()
).fold().toList() ).fold().toList()
# TODO: Load/update UI nodes on far drag nodes: list = await graph.V().valueMap(True).toList()
# 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 return nodes, edges
@@ -155,13 +149,10 @@ def node_to_blueprint_entity(ui_entity_element, node) -> None:
if entity_label not in k: if entity_label not in k:
ui_entity_element[k] = node[v][0] 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 = [] nodes = []
edges_data = [] edges_data = []
data_nodes, edges = await load_nodes_from_db(project_uuid, viewport_event) data_nodes, edges = await load_initial_graph(project_uuid)
if is_initial_read:
EntityRegistry.discover_plugins()
tmp_invalid_fix = [] tmp_invalid_fix = []
tmp_invalid_id_fix = [] tmp_invalid_id_fix = []
for node in data_nodes: for node in data_nodes:
@@ -171,9 +162,9 @@ async def read_graph(viewport_event, send_json, project_uuid, is_initial_read: b
} }
entity_id = node.pop(T.id) entity_id = node.pop(T.id)
entity_type = node.pop(T.label) entity_type = node.pop(T.label)
plugin = await EntityRegistry.get_plugin(to_snake_case(entity_type)) plugin = await Registry.get_plugin(to_snake_case(entity_type))
if plugin: if plugin:
ui_entity = plugin.create() ui_entity = plugin.blueprint()
for element in ui_entity['elements']: for element in ui_entity['elements']:
if isinstance(element, list): if isinstance(element, list):
[node_to_blueprint_entity( [node_to_blueprint_entity(
@@ -196,8 +187,8 @@ async def read_graph(viewport_event, send_json, project_uuid, is_initial_read: b
} }
nodes.append(ui_entity) nodes.append(ui_entity)
else: else:
# TODO detect invalid/renamed entities/plugins on any plugin label updates and fix graph
tmp_invalid_id_fix.append(entity_id) tmp_invalid_id_fix.append(entity_id)
# TODO detect invalid/renamed entities/plugins on any plugin label updates and fix graph
tmp_invalid_fix.append(entity_type) 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}') print(f'(TODO: on entity code update and label change rename nodes in janusgraph) Error Invalid Plugin: {entity_type}')
if len(edges[0]) > 0: if len(edges[0]) > 0:
@@ -216,24 +207,22 @@ async def read_graph(viewport_event, send_json, project_uuid, is_initial_read: b
'type': 'float' 'type': 'float'
}) })
await send_json({ await send_json({
'action': 'isInitialRead' if is_initial_read else 'read', 'action': 'isInitialRead',
'nodes': nodes, 'nodes': nodes,
'edges': edges_data 'edges': edges_data
}) })
async def update_node(node, send_json, uuid: UUID): async def update_node(node, action_type, send_json, uuid: UUID):
if updateTargetId := node.pop('id', None): if updateTargetId := node.pop('id', None):
async with ProjectGraphConnection(uuid) as graph: async with ProjectGraphConnection(uuid) as graph:
updateTarget = graph.V(updateTargetId) updateTarget = graph.V(updateTargetId)
for k, v in node.items(): 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) updateTarget.property(Cardinality.single, to_snake_case(k), v)
await updateTarget.next() await updateTarget.next()
async def remove_nodes(node, send_json, uuid: UUID): async def remove_nodes(node, action_type, send_json, uuid: UUID):
async with ProjectGraphConnection(uuid) as graph: async with ProjectGraphConnection(uuid) as graph:
if targetNode := node.get('id'): if targetNode := node.get('id'):
await graph.V(targetNode).drop().next() await graph.V(targetNode).drop().next()
@@ -254,20 +243,23 @@ async def get_transform_notification(transform_output, transform_type):
async def nodes_transform( async def nodes_transform(
entity_context: dict, node: dict,
send_json: Callable[[dict], None], send_json: Callable[[dict], None],
uuid: UUID uuid: UUID
): ):
transform_result = {} transform_result = {}
transform_type = entity_context.get("transform") entity_type = node.get('data', {}).get('label')
entity_data = entity_context.get("data", {}) transform_type = None
entity_type = entity_data.get("label") plugin = await Registry.get_plugin(entity_type)
plugin = await EntityRegistry.get_plugin(entity_type)
if plugin := plugin(): if plugin := plugin():
transform_result = await plugin.run_transform( transform_type = node["transform"]
transform_result = await plugin.get_transform(
transform_type=transform_type, transform_type=transform_type,
transform_context=entity_context, entity=node,
use=TransformUse(get_driver=deps.get_driver, config=EntityConfig(enabled=False, test=1, test2="two")) use=PluginUse(
get_driver=deps.get_driver,
get_graph=lambda: None
)
) )
async def create_entity_data( async def create_entity_data(
graph: AsyncGraphTraversal, graph: AsyncGraphTraversal,
@@ -306,15 +298,13 @@ async def nodes_transform(
async with ProjectGraphConnection(uuid) as graph: async with ProjectGraphConnection(uuid) as graph:
if isinstance(transform_result, list): if isinstance(transform_result, list):
out_result = [await create_entity_data(graph, result, entity_context) for result in transform_result] out_result = [await create_entity_data(graph, result, node) for result in transform_result]
else: else:
out_result = list(await create_entity_data(graph, transform_result, entity_context)) out_result = list(await create_entity_data(graph, transform_result, node))
transform_notification = await get_transform_notification(transform_result, transform_type)
await send_json({ await send_json({
"action": "isLoading", "action": "isLoading",
"detail": False, "detail": False,
"message": transform_notification "message": await get_transform_notification(transform_result, transform_type)
}) })
await send_json({ await send_json({
"action": "createEntity", "action": "createEntity",
@@ -324,115 +314,101 @@ async def nodes_transform(
await send_json({'error': 'noPluginFound'}) await send_json({'error': 'noPluginFound'})
async def run_user_graph_event(event: dict, send_json: Callable, uuid: UUID) -> None: async def get_command_type(event):
user_event: list = event["action"].split(":") user_event: list = event["action"].split(":")
action = user_event[0] action = user_event[0]
IS_READ: bool = action == "read" USER_ACTION = None
IS_UPDATE: bool = action == "update" if len(user_event) >= 2:
IS_DELETE: bool = action == "delete" USER_ACTION = user_event[1]
IS_TRANSFORM: bool = action == "transform" IS_READ = action == "read"
IS_INITIAL: bool = 'initial' in action IS_UPDATE = action == "update"
IS_DELETE = action == "delete"
IS_TRANSFORM = action == "transform"
return USER_ACTION, IS_READ, IS_UPDATE, IS_DELETE, IS_TRANSFORM
ACTION_TARGET = user_event[1]
if ACTION_TARGET == 'node': 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 IS_UPDATE: if IS_UPDATE:
await update_node(event["node"], send_json, uuid) await update_node(event["node"], USER_ACTION, send_json, uuid)
if IS_DELETE: if IS_DELETE:
await remove_nodes(event["node"], send_json, uuid) await remove_nodes(event["node"], USER_ACTION, send_json, uuid)
if IS_TRANSFORM: if IS_TRANSFORM:
await send_json({"action": "isLoading", "detail": True }) await send_json({"action": "isLoading", "detail": True })
await nodes_transform(event["node"], send_json, uuid) 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}") @router.websocket("/graph/{hid}")
async def active_graph_inquiry( async def active_graph_inquiry(
websocket: WebSocket, websocket: WebSocket,
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_ws)], user: Annotated[schemas.User, Depends(deps.get_user_from_ws)],
hid: Annotated[int, Depends(deps.get_graph_id)], hid: Annotated[int, Depends(deps.get_graph_id)],
db: Session = Depends(deps.get_db) db: Session = Depends(deps.get_db)
): ):
EntityRegistry.discover_plugins()
graph_users[user.cid.hex] = websocket
active_inquiry = crud.graphs.get(db, id=hid)
await websocket.accept() await websocket.accept()
if active_inquiry is not None: is_project_active = True
await graph_users[user.cid.hex].send_json({"action": "isLoading", "detail": True }) active_inquiry = crud.graphs.get(db, id=hid)
if active_inquiry is None:
is_project_active = False
else: else:
websocket.send({"action": "error"}) await websocket.send_json({"action": "isLoading", "detail": True })
for user_cid, ws in graph_users.items(): while is_project_active:
try: try:
async for event in ws.iter_json(): event: dict = await websocket.receive_json()
await run_user_graph_event( await run_user_graph_event(
event=event, event=event,
send_json=ws.send_json, send_json=websocket.send_json,
uuid=active_inquiry.uuid uuid=active_inquiry.uuid
) )
except OBPluginError as e: except OBPluginError as e:
await ws.send_json({"action": "error", "detail": f"{e}"}) await websocket.send_json({"action": "error", "detail": f"{e}"})
await ws.send_json({"action": "isLoading", "detail": False }) await websocket.send_json({"action": "isLoading", "detail": False })
log.error(e) log.error(e)
except (WebSocketException, ConnectionClosedError) as e: except (WebSocketException, BufferError, ConnectionClosedError) as e:
await websocket.send_json({"action": "isLoading", "detail": False })
log.error("Exception inside node.active_project") log.error("Exception inside node.active_project")
log.error(e) log.error(e)
await ws.send_json({"action": "isLoading", "detail": False }) is_project_active = False
await ws.close() await websocket.close()
# del graph_users[user_cid] except WebSocketDisconnect as e:
except (WebSocketDisconnect, RuntimeError) as e: log.info(f"disconnected!")
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") @router.get("/refresh")
async def refresh_entity_plugins( async def refresh_plugins(
hid: Annotated[str, Depends(deps.get_graph_id)], hid: Annotated[str, Depends(deps.get_graph_id)],
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)], user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
db: Session = Depends(deps.get_db) db: Session = Depends(deps.get_db)
): ):
# try: try:
EntityRegistry.discover_plugins() refresh_local_entities(db)
return {"status": "success", "plugins": EntityRegistry._visible_entities} return {"status": "success", "plugins": Registry.ui_labels}
# except Exception as e: except Exception as e:
# log.error("Error inside node.refresh_plugins") log.error("Error inside node.refresh_plugins")
# log.error(e) log.error(e)
# raise HTTPException( raise HTTPException(
# status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
# detail="There was an error refreshing, please try again." detail="There was an error refreshing, please try again."
# ) )
@router.post("/") @router.post("/")
async def create_entity_on_drop( async def create_entity_on_drop(
hid: Annotated[str, Depends(deps.get_graph_id)], hid: Annotated[str, Depends(deps.get_graph_id)],
user: Annotated[schemas.UserInDBBase, Depends(deps.get_user_from_session)], user: Annotated[schemas.User, Depends(deps.get_user_from_session)],
create_node: schemas.CreateNode, create_node: schemas.CreateNode,
db: Session = Depends(deps.get_db) db: Session = Depends(deps.get_db)
): ):
try: try:
active_inquiry = crud.graphs.get(db, id=hid) active_inquiry = crud.graphs.get(db, id=hid)
plugin = await EntityRegistry.get_plugin(plugin_label=to_snake_case(create_node.label)) plugin = await Registry.get_plugin(plugin_label=to_snake_case(create_node.label))
if plugin: if plugin:
blueprint = plugin.create() blueprint = plugin.blueprint()
blueprint["position"] = create_node.position.model_dump() blueprint["position"] = create_node.position.model_dump()
return await save_node_on_drop( return await save_node_on_drop(
create_node.label, create_node.label,

View File

@@ -1,12 +1,9 @@
import datetime
from typing import Generator, Any from typing import Generator, Any
from contextlib import contextmanager from contextlib import contextmanager
import uuid
import boto3 import boto3
from botocore.exceptions import BotoCoreError from botocore.exceptions import BotoCoreError
from fastapi import Depends, HTTPException, WebSocket, WebSocketException from fastapi import Depends, HTTPException, WebSocket, WebSocketException
from fastapi.encoders import jsonable_encoder
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
import undetected_chromedriver as uc import undetected_chromedriver as uc
from selenium import webdriver from selenium import webdriver
@@ -16,16 +13,13 @@ from selenium.webdriver.chrome.webdriver import WebDriver
from app import schemas, crud from app import schemas, crud
from app.db.session import SessionLocal from app.db.session import SessionLocal
from app.core.logger import get_logger from app.core.logger import get_logger
from app.api.utils import APIRequest, HidChecker, get_hid from app.api.utils import APIRequest, HidChecker, hid
from app.core.config import settings from app.core.config import settings
log = get_logger("api.deps") log = get_logger("api.deps")
if "dev" in settings.ENVIRONMENT:
___OB_DEV_CID___ = uuid.uuid4()
def get_db() -> Generator[Any, Session, Any]: def get_db() -> Generator[Any, Session, Any]:
try: try:
db: Session = SessionLocal() db: Session = SessionLocal()
@@ -37,56 +31,26 @@ def get_db() -> Generator[Any, Session, Any]:
async def get_user_from_session( async def get_user_from_session(
request: APIRequest, request: APIRequest,
db: Session = Depends(get_db) db: Session = Depends(get_db)
) -> schemas.UserInDBBase: ) -> schemas.User:
if 'dev' in settings.ENVIRONMENT:
return {}
if user := request.session.get("member"): if user := request.session.get("member"):
ob_user: schemas.UserInDBBase | None = crud.user.get_by_cid(db=db, cid=user.get("id")) ob_user: schemas.User | None = crud.user.get_by_cid(db=db, cid=user.get("id"))
if ob_user: if ob_user:
ob_user = jsonable_encoder(ob_user) return 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") raise HTTPException(status_code=401, detail="Unauthorized")
async def get_user_from_ws( async def get_user_from_ws(
websocket: WebSocket, websocket: WebSocket,
db: Session = Depends(get_db) db: Session = Depends(get_db)
) -> schemas.UserInDBBase: ) -> schemas.User:
if 'dev' in settings.ENVIRONMENT:
return {}
if user := websocket.session.get("member"): if user := websocket.session.get("member"):
ob_user: schemas.UserInDBBase | None = crud.user.get_by_cid(db=db, cid=user.get("id")) ob_user: schemas.User | None = crud.user.get_by_cid(db=db, cid=user.get("id"))
if ob_user: if ob_user:
ob_user = jsonable_encoder(ob_user) return 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) raise WebSocketException(code=401)

View File

@@ -34,7 +34,7 @@ if TYPE_CHECKING:
sqids = Sqids(alphabet=settings.SQIDS_ALPHABET, min_length=4) sqids = Sqids(alphabet=settings.SQIDS_ALPHABET, min_length=4)
def get_hid(db_id: int, ns: int = settings.SQIDS_NAMESPACE) -> str: def hid(db_id: int, ns: int = settings.SQIDS_NAMESPACE) -> str:
"""Generate an ID thats easy for humans to work with""" """Generate an ID thats easy for humans to work with"""
return sqids.encode([ns, db_id]) return sqids.encode([ns, db_id])

View File

@@ -1,6 +1,5 @@
from typing import List from typing import List
from fastapi.encoders import jsonable_encoder
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import func, select from sqlalchemy import func, select
@@ -32,11 +31,6 @@ class CRUDEntities(CRUDBase[
) -> List[ModelType]: ) -> List[ModelType]:
return db.query(self.model).where(self.model.uuid == uuid).first() 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): def update_favorite_by_id(self, db: Session, db_obj: Entities, is_favorite: bool = False):
setattr(db_obj, 'is_favorite', is_favorite) setattr(db_obj, 'is_favorite', is_favorite)
db.add(db_obj) db.add(db_obj)

View File

@@ -45,7 +45,7 @@ class CRUDGraphs(CRUDBase[
def get_many_user_graphs( def get_many_user_graphs(
self, self,
db: Session, db: Session,
user: schemas.UserInDBBase, user: schemas.User,
skip: int = 0, skip: int = 0,
limit: int = 50, limit: int = 50,
is_favorite: bool = True is_favorite: bool = True

View File

@@ -1,8 +1,6 @@
import inspect, time, importlib, imp
import requests import requests
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from osintbuddy.plugins import EntityRegistry from osintbuddy import load_plugin, Registry
from app import crud, schemas from app import crud, schemas
from app.core.logger import get_logger from app.core.logger import get_logger
@@ -11,61 +9,56 @@ from .base import * # noqa
log = get_logger(__name__) log = get_logger(__name__)
core_ob_url = 'https://raw.githubusercontent.com/jerlendds/osintbuddy-core-plugins/develop/plugins/' core_ob_url = 'https://raw.githubusercontent.com/jerlendds/osintbuddy-core-plugins/main/plugins/'
core_entity_files: list[str] = [ core_plugins = {
'google_cse_result', 'CSE Result': 'cse_result',
'google_cse_search', 'CSE Search': 'cse_search',
'dns', 'DNS': 'dns',
'google_cache_result', 'Google Cache Result': 'google_cache_result',
'google_cache_search', 'Google Cache Search': 'google_cache_search',
'google_result', 'Google Result': 'google_result',
'google_search', 'Google Search': 'google_search',
'ip', 'IP': 'ip',
'ip_geolocation', 'IP Geolocation': 'ip_geolocation',
'subdomain', 'Subdomain': 'subdomain',
'telegram_cse_search', 'Telegram Websearch': 'telegram_websearch',
'url', 'URL': 'url',
'username', 'Username': 'username',
'username_profile', 'Username Profile': 'username_profile',
'website', 'Website': 'website',
'whois' 'Whois': '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: def init_db(db: Session) -> None:
entity_count = crud.entities.count_all(db)[0][0] entity_count = crud.entities.count_all(db)[0][0]
if entity_count < 14: if entity_count < 14:
for file_endpoint in core_entity_files: for plugin_label, plugin_mod in core_plugins.items():
log.info(f"Saving core plugin/entity file: {file_endpoint}") log.info(f'Loading core plugins: {plugin_label}')
try: try:
resp = requests.get(core_ob_url + file_endpoint + ".py") resp = requests.get(core_ob_url + plugin_mod + '.py')
source_file = open(f"/app/app/plugins/{file_endpoint}.py", "w+") load_initial_plugin(db=db, plugin_mod=plugin_mod, plugin_code=resp.text)
source_file.write(resp.text)
source_file.close()
except requests.exceptions.ConnectionError as e: except requests.exceptions.ConnectionError as e:
# TODO: Use tenacity lib retry logic instead log.error(e)
log.error("Error loading core plugin! ", e) resp = requests.get(core_ob_url + plugin_mod + '.py')
load_initial_plugin(db=db, plugin_mod=plugin_mod, plugin_code=resp.text)
EntityRegistry.discover_plugins() except Exception:
for e in EntityRegistry.entities: pass
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 { return {
"status": "success", "status": "success",
"service": "[Entities: Create]", "service": "[Entities: Create]",
"message": f"{len(EntityRegistry.entities)} initial entities (plugins) loaded", "message": f"{len(Registry.plugins)} initial entities (plugins) loaded",
} }

View File

@@ -1,14 +1,9 @@
import os
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from app.core.config import settings 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) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

View File

@@ -0,0 +1,15 @@
<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>

View File

@@ -0,0 +1,19 @@
<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>

View File

@@ -0,0 +1,11 @@
<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>

View File

@@ -14,12 +14,12 @@ def init() -> None:
def main() -> None: def main() -> None:
# try: try:
log.info("Running initial data check...") log.info("Running initial data check...")
init() init()
log.info("Initial data check completed.") log.info("Initial data check completed.")
# except Exception as e: except Exception as e:
# log.error(e) log.error(e)
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -8,7 +8,7 @@ from starlette.middleware.sessions import SessionMiddleware
# from fastapi_cache.backends.redis import CACHE_KEY, RedisCacheBackend # from fastapi_cache.backends.redis import CACHE_KEY, RedisCacheBackend
# import sentry_sdk # import sentry_sdk
# from sentry_sdk.integrations.asyncio import AsyncioIntegration # from sentry_sdk.integrations.asyncio import AsyncioIntegration
from app.api.api_v1.api import api_router, graph_inquiry from app.api.api_v1.api import api_router, node
from app.core.config import settings, use_route_names_as_operation_ids from app.core.config import settings, use_route_names_as_operation_ids
from app.core.casdoor import Config as CasdoorConfig from app.core.casdoor import Config as CasdoorConfig
# if settings.SENTRY_DSN: # if settings.SENTRY_DSN:

View File

@@ -3,7 +3,7 @@ from uuid import UUID
from typing import Optional, List from typing import Optional, List
from pydantic import BaseModel, validator, ConfigDict from pydantic import BaseModel, validator, ConfigDict
from app.api.utils import get_hid from app.api.utils import hid
from osintbuddy.templates import plugin_source_template from osintbuddy.templates import plugin_source_template
ENTITY_NAMESPACE = 1510 ENTITY_NAMESPACE = 1510
@@ -35,10 +35,10 @@ class PostEntityCreate(BaseModel):
class EntityUpdate(EntityBase): class EntityUpdate(EntityBase):
label: Optional[str] = "" label: Optional[str]
author: Optional[str] = "" author: Optional[str]
description: Optional[str] = "" description: Optional[str]
source: Optional[str] source: Optional[str]
class EntityInDBBase(EntityBase): class EntityInDBBase(EntityBase):
@@ -47,7 +47,7 @@ class EntityInDBBase(EntityBase):
'id', 'id',
pre=True, pre=True,
allow_reuse=True allow_reuse=True
)(lambda v: get_hid(v, ENTITY_NAMESPACE)) )(lambda v: hid(v, ENTITY_NAMESPACE))
last_edited: datetime.datetime last_edited: datetime.datetime
updated: datetime.datetime updated: datetime.datetime
@@ -55,8 +55,7 @@ class EntityInDBBase(EntityBase):
class Entity(EntityInDBBase): class Entity(EntityInDBBase):
# model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
pass
class EntityInList(PostEntityCreate): class EntityInList(PostEntityCreate):

View File

@@ -3,7 +3,7 @@ from uuid import UUID
from typing import Optional, List from typing import Optional, List
from pydantic import BaseModel, validator from pydantic import BaseModel, validator
from app.api.utils import get_hid, sqids from app.api.utils import hid, sqids
GRAPH_NAMESPACE = 5172 GRAPH_NAMESPACE = 5172
@@ -31,7 +31,7 @@ class GraphInDBBase(GraphsBase):
'id', 'id',
pre=True, pre=True,
allow_reuse=True allow_reuse=True
)(lambda v: get_hid(v, GRAPH_NAMESPACE)) )(lambda v: hid(v, GRAPH_NAMESPACE))
class Config: class Config:
from_orm = True from_orm = True

View File

@@ -72,6 +72,9 @@ class User(UserCreate):
class UserInDBBase(UserCreate): class UserInDBBase(UserCreate):
is_admin: bool
created_time: datetime.datetime
updated_time: datetime.datetime
cid: UUID cid: UUID
class Config: class Config:

View File

@@ -1,16 +1,10 @@
import osintbuddy as ob import osintbuddy
from osintbuddy import load_local_plugins
from app import crud, schemas, models from app import crud, schemas, models
from app.core.logger import get_logger from app.core.logger import get_logger
from app.db.session import SessionLocal
from termcolor import colored from termcolor import colored
from pyfiglet import figlet_format from pyfiglet import figlet_format
db = SessionLocal()
entities = crud.entities.get_many(db, skip=0, limit=100)
load_local_plugins(entities, "plugins")
print( print(
colored(figlet_format("osintbuddy py >", font='smslant',), color="green"), colored(figlet_format("osintbuddy py >", font='smslant',), color="green"),
colored("Created by jerlendds", color="blue"), colored("Created by jerlendds", color="blue"),
@@ -18,11 +12,6 @@ print(
############################################################################### ###############################################################################
# The following imports and variables have been included in your current shell: # The following imports and variables have been included in your current shell:
############################################################################### ###############################################################################
import osintbuddy as ob import osintbuddy
from osintbuddy import load_local_plugins
from app import crud, schemas, models 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")) """, color="green"))

View File

@@ -1,4 +1,4 @@
FROM python:3.11.7-slim-bullseye FROM python:3.11.4-slim-bullseye
LABEL maintainer="jerlendds <support@forum.osintbuddy.com>" LABEL maintainer="jerlendds <support@forum.osintbuddy.com>"
WORKDIR /app/ WORKDIR /app/

View File

@@ -28,8 +28,7 @@ watchfiles==0.19.0
gremlinpy @ git+https://github.com/jerlendds/gremlinpy.git@eaba7dca12ad0156eb0d6d8ba2eb5751551c6a6d gremlinpy @ git+https://github.com/jerlendds/gremlinpy.git@eaba7dca12ad0156eb0d6d8ba2eb5751551c6a6d
warcio==1.7.4
sqids==0.4.0 sqids==0.4.0
sentry-sdk==1.39.1 sentry-sdk==1.33.1
boto3-stubs==1.28.75 boto3-stubs==1.28.75

View File

@@ -1 +0,0 @@
*.py

View File

@@ -28,6 +28,5 @@ watchfiles==0.19.0
gremlinpy @ git+https://github.com/jerlendds/gremlinpy.git@eaba7dca12ad0156eb0d6d8ba2eb5751551c6a6d gremlinpy @ git+https://github.com/jerlendds/gremlinpy.git@eaba7dca12ad0156eb0d6d8ba2eb5751551c6a6d
warcio==1.7.4
sqids==0.4.0 sqids==0.4.0
sentry-sdk==1.39.1 sentry-sdk==1.33.1

View File

@@ -3,6 +3,9 @@
"version": "0.3.0", "version": "0.3.0",
"private": true, "private": true,
"dependencies": { "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", "@codemirror/lang-python": "^6.1.3",
"@headlessui/react": "^1.7.7", "@headlessui/react": "^1.7.7",
"@heroicons/react": "^2.0.13", "@heroicons/react": "^2.0.13",
@@ -10,7 +13,6 @@
"@reactour/tour": "^3.6.1", "@reactour/tour": "^3.6.1",
"@reduxjs/toolkit": "^1.9.1", "@reduxjs/toolkit": "^1.9.1",
"@tabler/icons-react": "^2.42.0", "@tabler/icons-react": "^2.42.0",
"@tarii/3d-earth": "^1.0.3",
"@uiw/codemirror-theme-aura": "^4.21.20", "@uiw/codemirror-theme-aura": "^4.21.20",
"@uiw/react-codemirror": "^4.21.20", "@uiw/react-codemirror": "^4.21.20",
"casdoor-js-sdk": "^0.11.0", "casdoor-js-sdk": "^0.11.0",

View File

@@ -1,9 +1,9 @@
import { emptyApi as api } from "./baseApi"; import { emptyApi as api } from "./baseApi";
const injectedRtkApi = api.injectEndpoints({ const injectedRtkApi = api.injectEndpoints({
endpoints: (build) => ({ endpoints: (build) => ({
refreshEntityPlugins: build.query< refreshPlugins: build.query<
RefreshEntityPluginsApiResponse, RefreshPluginsApiResponse,
RefreshEntityPluginsApiArg RefreshPluginsApiArg
>({ >({
query: (queryArg) => ({ query: (queryArg) => ({
url: `/api/v1/node/refresh`, url: `/api/v1/node/refresh`,
@@ -125,7 +125,7 @@ const injectedRtkApi = api.injectEndpoints({
query: (queryArg) => ({ query: (queryArg) => ({
url: `/api/v1/entity/${queryArg.hid}`, url: `/api/v1/entity/${queryArg.hid}`,
method: "PUT", method: "PUT",
body: queryArg.entityUpdate, body: queryArg.entityBase,
}), }),
}), }),
deleteEntity: build.mutation<DeleteEntityApiResponse, DeleteEntityApiArg>({ deleteEntity: build.mutation<DeleteEntityApiResponse, DeleteEntityApiArg>({
@@ -176,9 +176,9 @@ const injectedRtkApi = api.injectEndpoints({
overrideExisting: false, overrideExisting: false,
}); });
export { injectedRtkApi as api }; export { injectedRtkApi as api };
export type RefreshEntityPluginsApiResponse = export type RefreshPluginsApiResponse =
/** status 200 Successful Response */ any; /** status 200 Successful Response */ any;
export type RefreshEntityPluginsApiArg = { export type RefreshPluginsApiArg = {
hid: string; hid: string;
}; };
export type CreateEntityOnDropApiResponse = export type CreateEntityOnDropApiResponse =
@@ -200,7 +200,7 @@ export type PostSignoutApiResponse =
/** status 200 Successful Response */ Status; /** status 200 Successful Response */ Status;
export type PostSignoutApiArg = void; export type PostSignoutApiArg = void;
export type GetAccountApiResponse = /** status 200 Successful Response */ export type GetAccountApiResponse = /** status 200 Successful Response */
| UserInDbBase | User
| HttpError; | HttpError;
export type GetAccountApiArg = void; export type GetAccountApiArg = void;
export type GetGraphApiResponse = /** status 200 Successful Response */ Graph; export type GetGraphApiResponse = /** status 200 Successful Response */ Graph;
@@ -264,7 +264,7 @@ export type UpdateEntityByIdApiResponse =
/** status 200 Successful Response */ any; /** status 200 Successful Response */ any;
export type UpdateEntityByIdApiArg = { export type UpdateEntityByIdApiArg = {
hid: string; hid: string;
entityUpdate: EntityUpdate; entityBase: EntityBase;
}; };
export type DeleteEntityApiResponse = /** status 200 Successful Response */ any; export type DeleteEntityApiResponse = /** status 200 Successful Response */ any;
export type DeleteEntityApiArg = { export type DeleteEntityApiArg = {
@@ -313,7 +313,7 @@ export type Status = {
export type HttpError = { export type HttpError = {
detail: string; detail: string;
}; };
export type UserInDbBase = { export type User = {
name: string; name: string;
username?: string | null; username?: string | null;
email?: string | null; email?: string | null;
@@ -325,7 +325,7 @@ export type UserInDbBase = {
is_admin: boolean; is_admin: boolean;
created_time: string; created_time: string;
updated_time: string; updated_time: string;
cid: string; uuid: string;
}; };
export type Graph = { export type Graph = {
label: string; label: string;
@@ -381,11 +381,11 @@ export type PostEntityCreate = {
author: string; author: string;
description: string; description: string;
}; };
export type EntityUpdate = { export type EntityBase = {
label?: string | null; label?: string;
author?: string | null; author?: string;
description?: string | null; description?: string;
source: string | null; source?: string;
is_favorite?: boolean; is_favorite?: boolean;
}; };
export type ScanMachineCreate = { export type ScanMachineCreate = {
@@ -393,7 +393,7 @@ export type ScanMachineCreate = {
description: string; description: string;
}; };
export const { export const {
useRefreshEntityPluginsQuery, useRefreshPluginsQuery,
useCreateEntityOnDropMutation, useCreateEntityOnDropMutation,
useGetCasdoorConfigQuery, useGetCasdoorConfigQuery,
usePostSigninMutation, usePostSigninMutation,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -1,254 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 33 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 4.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 3.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

2
frontend/src/assets/images/logo.svg Normal file → Executable file

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -1,29 +1,17 @@
.btn-primary { .btn-primary {
@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 ; @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;
} }
.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-primary svg,
.btn-danger svg { .btn-danger svg {
@apply ml-4 w-5 h-5 transition-transform duration-150 ease-out @apply ml-5 w-5 h-5
}
.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 { .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 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 @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
} }
.btn-form { .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 @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
} }
@@ -34,10 +22,11 @@
@apply mx-2 @apply mx-2
} }
.form-input-wrapper { .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 @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 { .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
} }

View File

@@ -1,9 +1,6 @@
.bar-hz .ct-label { .bar-hz .ct-label {
@apply text-slate-400 font-display @apply text-slate-400 font-display
} }
.bar-hz {
@apply overflow-y-scroll flex flex-col
}
.bar-hz .ct-series-a .ct-bar { .bar-hz .ct-series-a .ct-bar {
@apply stroke-sky-600 @apply stroke-sky-600
@@ -36,7 +33,4 @@
} }
.bar-hz .ct-series-j .ct-bar { .bar-hz .ct-series-j .ct-bar {
@apply stroke-slate-200 @apply stroke-slate-200
}
.ct-label.ct-vertical.ct-end {
} }

View File

@@ -66,7 +66,7 @@
color: rgb(203, 213, 225); color: rgb(203, 213, 225);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
font-feature-settings: 'ss01'; font-feature-settings: 'ss01';
font-size: 0.8rem; font-size: 14px;
font-weight: 600; font-weight: 600;
line-height: 24.5px; line-height: 24.5px;
tab-size: 4; tab-size: 4;

View File

@@ -50,28 +50,20 @@ button {
::selection { ::selection {
@apply text-slate-200 bg-radiance-500/50 @apply text-slate-200 bg-radiance-500/50
} }
/* 2d6cffb0 */
html { html {
background-color: rgb(7, 21, 35, 1);
background-image: url(../images/image-bg.siteorigin.com-inflicted-overlay.png );
}
/* 1hmfchzd.png */
body {
background-color: transparent;
background-size: 100% 100%; background-size: 100% 100%;
background-position: 0px 0px,0px 0px,0px 0px,0px 0px,0px 0px; 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%);} 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%);
/* https://gradienta.io/editor */ }
/* https://gradienta.io/editor */
body {
@apply min-h-screen from-mirage-800/90 to-mirage-700/50 bg-gradient-to-br;
}
#root { #root {
@apply flex flex-col h-full min-h-screen relative bg-mirage-900/20 justify-between; @apply flex flex-col min-h-screen relative bg-mirage-800/60
}
.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 { svg.icon {
@@ -92,7 +84,6 @@ button,
a, a,
figcaption { figcaption {
@apply text-mirage-400 font-display; @apply text-mirage-400 font-display;
/* color: #ff9602; */
} }
/* /*
@@ -100,23 +91,23 @@ figcaption {
Works on Chrome, Edge, and Safari Works on Chrome, Edge, and Safari
*/ */
*::-webkit-scrollbar { *::-webkit-scrollbar {
width: 10px; width: 8px;
border-radius: none; border-radius: none;
} }
*::-webkit-scrollbar-track { *::-webkit-scrollbar-track {
background: #19233433; background: #19233433;
width: 10px; width: 8px;
border-radius: none; border-radius: none;
} }
*::-webkit-scrollbar-thumb { *::-webkit-scrollbar-thumb {
background-color: #1a1f2793; background-color: #1a1f2793;
border-radius: 2px; border-radius: 2px;
border-right: 9px solid #1b243a84; border-right: 10px solid #1b243a84;
} }
* { * {
scrollbar-width: 10px; scrollbar-width: 8px;
scrollbar-color: #1d222f62 #202d4333; scrollbar-color: #1d222f62 #202d4333;
} }
@@ -209,7 +200,6 @@ input[type='number'] {
} }
.react-flow__attribution a { .react-flow__attribution a {
@apply !text-slate-500; @apply !text-slate-500;
} }
.react-flow__edge-path { .react-flow__edge-path {
@@ -303,5 +293,7 @@ input[type='file'] {
@apply !border-slate-600 !w-2 !h-2 @apply !border-slate-600 !w-2 !h-2
} }
.cm-content {
@apply font-code text-sm font-semibold
}
/* end react-grid-layout and codemirror */ /* end react-grid-layout and codemirror */

View File

@@ -1,28 +1,25 @@
.sidebar-link { .sidebar-link {
@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; @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;
} }
.sidebar-link.active { .sidebar-link.active {
@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; @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 ;
}
.sidebar-link.active span {
@apply !text-slate-300/80
} }
.sidebar-link.active button { .sidebar-link.active button {
@apply bg-mirage-500 border-mirage-300/40 ; @apply bg-mirage-500 hover:bg-mirage-600 border-mirage-300/40 ;
} }
.sidebar-link.active svg { .sidebar-link.active svg {
@apply text-primary-300 -ml-0.5 @apply text-slate-400
} }
.sidebar-link button { .sidebar-link button {
@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 @apply border border-mirage-400/40 transition-colors duration-200 hover:border-mirage-300 bg-mirage-500
} }
.sidebar-link button svg { .sidebar-link button svg {
@apply origin-center rotate-0 @apply text-slate-400
}
.sidebar-link:hover button svg,
.sidebar-link.active button svg {
@apply !rotate-180
} }
.sidebar-link button:hover svg {
@apply text-slate-300;
}

View File

@@ -1,6 +1,6 @@
/* Start node */ /* Start node */
.node.container { .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/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; @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;
} }
.node .side-header { .node .side-header {
@@ -23,6 +23,8 @@
@apply leading-4; @apply leading-4;
} }
.node .node-display h1, .node .node-display h1,
.node .node-display h2, .node .node-display h2,
.node .node-display p { .node .node-display p {
@@ -36,11 +38,11 @@
} }
.node .header { .node .header {
@apply flex h-full w-full items-center justify-between rounded-t-md text-slate-50/70 py-2 px-1 ; @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;
} }
.node .header svg { .node .header svg {
@apply h-6 w-6; @apply h-7 w-7;
} }
.text-container { .text-container {

View File

@@ -1,16 +1,15 @@
import classNames from "classnames"; import classNames from "classnames";
import { ReactComponent as OSINTBuddyLogomark } from "@images/logo-ext.svg"; // OSINTBuddy-text.svg import { ReactComponent as OSINTBuddyLogo } from "@images/logo.svg";
import { Link, NavLink } from "react-router-dom"; import { Link, NavLink } from "react-router-dom";
import HamburgerMenu from "./HamburgerMenu"; import HamburgerMenu from "./HamburgerMenu";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { CogIcon, DocumentMagnifyingGlassIcon, FolderOpenIcon, PlusIcon, HomeIcon } from "@heroicons/react/24/outline"; import { CogIcon, DocumentMagnifyingGlassIcon, FolderOpenIcon, PlusIcon, HomeIcon } from "@heroicons/react/24/outline";
import { Icon } from "./Icons";
const navigation = [ const navigation = [
{ name: <><span>Dashboard</span></>, to: "/dashboard", icon: HomeIcon }, { name: "Dashboard", 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: "Incidents *", to: "/incidents", icon: FolderOpenIcon },
{ name: <><span>Scans</span> <span className="half-grayscale ml-auto mr-2.5 opacity-30 right-0">🚧</span></>, to: "/scans", icon: DocumentMagnifyingGlassIcon }, { name: "Scans *", to: "/scans", icon: DocumentMagnifyingGlassIcon },
]; ];
interface AppLayoutSidebarProps { interface AppLayoutSidebarProps {
@@ -23,7 +22,7 @@ export default function AppLayoutSidebar({ showSidebar, toggleSidebar, setShowIn
return ( return (
<div <div
className={classNames( className={classNames(
"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", "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",
showSidebar ? "translate-x-0" : "-translate-x-52 border-r-2" showSidebar ? "translate-x-0" : "-translate-x-52 border-r-2"
)} )}
> >
@@ -35,7 +34,7 @@ export default function AppLayoutSidebar({ showSidebar, toggleSidebar, setShowIn
)} )}
> >
<Link to="/" replace> <Link to="/" replace>
<OSINTBuddyLogomark className="h-7 ml-1 w-auto fill-slate-400" /> <OSINTBuddyLogo className="h-7 w-auto fill-slate-400" />
</Link> </Link>
<HamburgerMenu <HamburgerMenu
@@ -45,75 +44,86 @@ export default function AppLayoutSidebar({ showSidebar, toggleSidebar, setShowIn
/> />
</div> </div>
<div className="flex flex-1 flex-col overflow-y-auto"> <div className="flex flex-1 flex-col overflow-y-auto">
<nav className="flex-1 flex flex-col"> <nav className="flex-1 flex py-4 pt-1 flex-col">
{navigation.map((item) => ( {navigation.map((item) => (
<NavLink <NavLink
key={item.to} key={item.name}
to={item.to} to={item.to}
className={({ isActive }) => className={({ isActive }) =>
classNames( classNames(
isActive && "active mx-2", isActive && "active",
"sidebar-link ", "sidebar-link",
!showSidebar ? "mx-0 ml-0 mr-px " : 'mr-2.5' !showSidebar && "mx-0"
) )
} }
> >
<item.icon <item.icon
className={classNames( className={classNames(
"transition-all mr-2 flex-shrink-0 h-6 w-6 duration-100", "transition-all",
location.pathname.includes(item.to) && "text-slate-400/30", location.pathname.includes(item.to)
? "text-slate-500"
: "",
"mr-3 flex-shrink-0 h-6 w-6 duration-100",
showSidebar showSidebar
? "translate-x-0" ? "translate-x-0"
: "translate-x-[12.75rem] " : "translate-x-[13.15rem]"
)} )}
aria-hidden="true"
/> />
{item.name} {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>
))} ))}
<NavLink <NavLink
to="/settings" to="/settings"
replace replace
className={({ isActive, }) => className={({ isActive }) =>
classNames( classNames(
!showSidebar ? "mx-0 ml-0 mr-px" : 'mr-2.5', isActive && "active",
"sidebar-link transition-all mt-auto sidebar-link text-slate-400/30 ", "sidebar-link mt-auto",
isActive && "active mx-2" !showSidebar && "mx-0"
) )
} }
> >
<CogIcon <CogIcon
className={classNames(
"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] "
)}
/>
<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( className={classNames(
"transition-all", "transition-all",
"mr-2 flex-shrink-0 h-6 w-6 duration-100", location.pathname.includes("settings")
showSidebar ? "text-primary"
? "translate-x-0" : "text-slate-600 group-hover:text-slate-300",
: "translate-x-[12.75rem] " "mr-3 flex-shrink-0 h-6 w-6 duration-100",
showSidebar ? "translate-x-0" : "translate-x-[13.16rem]"
)} )}
aria-hidden="true"
/> />
Discord Settings
</a> </NavLink>
</nav> </nav>
</div> </div>
</div> </div>

View File

@@ -12,36 +12,25 @@ import { UpdateEntityByIdApiArg } from '../../app/api';
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
export const auraTheme = auraInit({ export const tokyoNightTheme = auraInit({
settings: { settings: {
caret: "#cbd5ef", caret: "#c6c6c6",
background: 'rgba(0 0 0 0)', background: 'rgba(0 0 0 0)',
fontFamily: 'Fira Code', fontFamily: 'monospace',
gutterBackground: 'rgba(36, 46, 107, .17)', gutterBackground: 'rgba(36, 46, 77, .16)',
selectionMatch: 'rgba(76, 86, 107, .14)', selectionMatch: 'rgba(76, 86, 107, .00)',
lineHighlight: 'rgba(36, 46, 77, .20)', lineHighlight: 'rgba(36, 46, 77, .26)',
}, },
styles: [ styles: [{ tag: t.comment, color: "#6272a4" }],
{ 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) { export function CodeEditor({ code, setCode }: JSONObject) {
return ( return (
<CodeMirror <CodeMirror
theme={auraTheme} theme={tokyoNightTheme}
value={code} value={code}
onChange={(value) => setCode(value)} onChange={(value) => setCode(value)}
extensions={[python()]} extensions={[python()]}
className="text-sm font-semibold"
/> />
) )
} }
@@ -76,7 +65,7 @@ export default function EntityEditor({ activeEntity, refetchEntity }: EntityEdit
isResizable={true} isResizable={true}
> >
<div <div
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" 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"
key="b" key="b"
data-grid={{ data-grid={{
x: 0, x: 0,
@@ -89,7 +78,7 @@ export default function EntityEditor({ activeEntity, refetchEntity }: EntityEdit
minW: 4.5, minW: 4.5,
}} }}
> >
<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"> <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">
<li className="flex items-start"> <li className="flex items-start">
<div className="flex items-center"> <div className="flex items-center">
<span className="text-slate-500 font-display truncate"> <span className="text-slate-500 font-display truncate">
@@ -114,7 +103,7 @@ export default function EntityEditor({ activeEntity, refetchEntity }: EntityEdit
<div className="flex justify-between items-center w-full text-slate-400 "> <div className="flex justify-between items-center w-full text-slate-400 ">
<button <button
onClick={() => { onClick={() => {
updateEntityById({ hid: activeEntity?.id ?? "", entityUpdate: { source: code as string } }).then(() => toast.info( updateEntityById({ hid: activeEntity?.id ?? "", entityBase: { source: code } }).then(() => toast.info(
`The ${activeEntity?.label} entity has been saved.` `The ${activeEntity?.label} entity has been saved.`
)) ))
refetchEntity() refetchEntity()
@@ -138,8 +127,10 @@ export default function EntityEditor({ activeEntity, refetchEntity }: EntityEdit
</div> </div>
</li> </li>
</ol> </ol>
<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 "> <div className="container bg-t overflow-y-scroll h-full ">
<CodeEditor code={code} setCode={setCode} /> <div className="editor ">
<CodeEditor code={code} setCode={setCode} />
</div>
</div> </div>
</div> </div>
</ResponsiveGridLayout > </ResponsiveGridLayout >

View File

@@ -27,7 +27,7 @@ export function GridPanel({
return ( return (
<> <>
<div <div
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 " 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 "
> >
<ol className="text-sm flex select-none justify-between relative px-2 py-2"> <ol className="text-sm flex select-none justify-between relative px-2 py-2">
<li className="flex items-start"> <li className="flex items-start">

View File

@@ -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'> <div className='min-w-0 max-w-2xl flex-auto pt-3 lg:max-w-none lg:pr-0 px-2'>
<section> <section>
<header className='space-y-1'> <header className='space-y-1'>
{title && <h4 className='font-display text-sm font-medium text-primary-200'>{title}</h4>} {title && <h4 className='font-display text-sm font-medium text-primary-300'>{title}</h4>}
{header && ( {header && (
<h1 className='font-display leading-4 text-xl tracking-tight text-slate-400 dark:text-slate-300'>{header}</h1> <h1 className='font-display leading-4 text-xl tracking-tight text-slate-400 dark:text-slate-300'>{header}</h1>
)} )}

View File

@@ -2,10 +2,12 @@ import classNames from 'classnames';
import Sprite from '@assets/images/tabler-sprite.svg'; import Sprite from '@assets/images/tabler-sprite.svg';
export const Icon = ({ icon, className }: { icon: string, className?: string }) => { export const Icon = ({ icon, className }: { icon: string, className?: string }) => {
// TODO: Optimize this? // TODO: Remove sprite and figure this approach out somehow
// const svgIcon = import(`@tabler/icons/${icon}.svg`).then(x => x).catch(() => <>why no work?!</>)
return ( return (
<> <>
<svg className={className ? className : 'h-5 w-5'} fill="none" stroke="currentColor"> <svg className={className ? className : 'h-5 w-5'} fill='#fff' color='#fff'>
<use href={`${Sprite}#tabler-${icon}`} /> <use href={`${Sprite}#tabler-${icon}`} />
</svg> </svg>
</> </>

View File

@@ -23,18 +23,16 @@ export default function RoundLoader({ className }: RoundLoaderProps): React.Reac
export function UnderConstruction({ header, description, className = 'flex px-6' }: any) { export function UnderConstruction({ header, description, className = 'flex px-6' }: any) {
return ( return (
<div className={className}> <div className={className}>
<div className='bg-mirage-800/30 w-full block shadow rounded-b-lg '> <div className='bg-mirage-700/50 w-full block shadow sm:rounded-lg '>
<div className='border-b border-mirage-300 mx-4 py-5 sm:px-6'> <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-6 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap'>
<div className='ml-4 mt-2 w-full'> <div className='ml-4 mt-2'>
<h1 className='text-2xl w-full flex items-center justify-between'> <h1 className='font-display text-2xl tracking-tight text-slate-300'>
<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> Under Construction
<span className="half-grayscale opacity-30 right-0 ml-auto">🚧</span>
</h1> </h1>
</div> </div>
</div> </div>
</div> </div>
<section className='flex flex-col px-3 mb-6'> <section className='flex flex-col px-3 mb-6'>
<InquiryHeader className='z-10' title='New Feature' header={header} /> <InquiryHeader className='z-10' title='New Feature' header={header} />
<p className='my-3 ml-6 text-slate-400 max-w-xl'> <p className='my-3 ml-6 text-slate-400 max-w-xl'>

View File

@@ -5,8 +5,8 @@ import { Bars3Icon, BellIcon, XMarkIcon } from '@heroicons/react/24/outline';
import classNames from 'classnames'; import classNames from 'classnames';
import OSINTBuddyLogo from '@assets/images/logo.svg'; import OSINTBuddyLogo from '@assets/images/logo.svg';
import { Listbox } from '@headlessui/react' import { Listbox } from '@headlessui/react'
import { Icon } from './Icons'; import { GithubIcon } from './Icons';
import Logo from '@images/logo-ext.svg'; import Logo from '@images/logo.svg';
const themes = [ const themes = [
{ name: 'Light', value: 'light', icon: LightIcon }, { name: 'Light', value: 'light', icon: LightIcon },
@@ -60,15 +60,15 @@ export function ThemeSelector(props: any) {
} else { } else {
setSelectedTheme( setSelectedTheme(
// @ts-ignore // @ts-ignore
themes.find((theme) => theme.value === document.documentElement.getAttribute('data-theme'))) themes.find((theme) =>theme.value === document.documentElement.getAttribute('data-theme')))
} }
}, [selectedTheme]) }, [selectedTheme])
useEffect(() => { useEffect(() => {
let handler = () => let handler = () =>
setSelectedTheme( setSelectedTheme(
// @ts-ignore // @ts-ignore
themes.find((theme) => theme.value === (window.localStorage.theme ?? 'system'))) themes.find((theme) => theme.value === (window.localStorage.theme ?? 'system')))
window.addEventListener('storage', handler) window.addEventListener('storage', handler)
@@ -147,36 +147,28 @@ export default function PublicNavbar(): React.ReactElement {
}, []) }, [])
return ( return (
<header <header
className={classNames( className={classNames(
'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', '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',
isScrolled isScrolled
? 'dark:bg-slate-800/95 dark:backdrop-blur dark:[@supports(backdrop-filter:blur(0))]:bg-slate-900/75' ? 'dark:bg-slate-900/95 dark:backdrop-blur dark:[@supports(backdrop-filter:blur(0))]:bg-slate-900/75'
: 'dark:bg-transparent' : 'dark:bg-transparent'
)} )}
> >
<div className="flex lg:hidden"> <div className="mr-6 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 */} {/* @todo add mobile navigation */}
</div> </div>
<div className="relative flex flex-grow basis-0 items-center"> <div className="relative flex flex-grow basis-0 items-center">
<Link to="/"> <Link to="/">
<img className="hidden h-9 fill-slate-700 dark:fill-sky-100 lg:block" src={Logo} /> <img className="hidden h-9 w-auto fill-slate-700 dark:fill-sky-100 lg:block" src={Logo} />
</Link> </Link>
</div> </div>
<div className=""> <div className="-my-5 mr-6 sm:mr-8 md:mr-0">
{/* <Search /> */} {/* <Search /> */}
</div> </div>
<div className="ml-auto relative flex basis-0 justify-end mr-2 gap-6 sm:gap-8 md:flex-grow"> <div className="relative flex basis-0 justify-end 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"> <a href="https://github.com/jerlendds/osintbuddy" className="group" aria-label="GitHub">
<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" /> <GithubIcon className="h-6 w-6 fill-slate-400 group-hover:fill-slate-500 dark:group-hover:fill-slate-300" />
</a> </a>
</div> </div>
</header> </header>

View File

@@ -24,8 +24,6 @@ export interface Graph extends EditState {
project: ActiveProject; project: ActiveProject;
viewMode: ProjectViewModes; viewMode: ProjectViewModes;
positionMode: PositionModes positionMode: PositionModes
showEdges: false,
shownEdgesByNodeId: JSONObject[]
} }
const initialState: Graph = { const initialState: Graph = {
@@ -41,9 +39,7 @@ const initialState: Graph = {
name: '', name: '',
}, },
viewMode: 'edit', viewMode: 'edit',
positionMode: 'manual', positionMode: 'manual'
showEdges: false,
shownEdgesByNodeId: []
}; };

View File

@@ -36,7 +36,7 @@ export default function AppLayout() {
showSidebar ? "translate-x-64" : "translate-x-12" 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 /> <Outlet />
</main> </main>
</div> </div>

View File

@@ -1,5 +1,5 @@
import { ReactElement, Suspense, lazy } from "react"; import { ReactElement, Suspense, lazy } from "react";
import { Navigate, Route, RouterProvider, Routes, createBrowserRouter } from "react-router-dom"; import { Route, RouterProvider, Routes, createBrowserRouter } from "react-router-dom";
import NotFound from "./NotFound"; import NotFound from "./NotFound";
import AppLayout from "./AppLayout"; import AppLayout from "./AppLayout";
import PublicLayout from "./PublicLayout"; 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 EntityDetails = lazy(() => import("@routes/dashboard/_components/entity/EntityDetails"))
const Settings = lazy(() => import("@routes/settings")); const Settings = lazy(() => import("@routes/settings"));
const Workspaces = lazy(() => import("@src/routes/workspaces")); const Incidents = lazy(() => import("@routes/incidents"));
const ScansCreate = lazy(() => import("@routes/scans-create")) const ScansCreate = lazy(() => import("@routes/scans-create"))
const ScansOverview = lazy(() => import("@routes/scans")); const ScansOverview = lazy(() => import("@routes/scans"));
@@ -58,10 +58,6 @@ const router = createBrowserRouter([
path: 'dashboard', path: 'dashboard',
element: <Suspense><DashboardPage /></Suspense>, element: <Suspense><DashboardPage /></Suspense>,
children: [ children: [
{
path: '',
element: <Navigate to="graph" replace />
},
{ {
path: 'market', path: 'market',
element: <Suspense><Market /></Suspense> element: <Suspense><Market /></Suspense>
@@ -93,8 +89,8 @@ const router = createBrowserRouter([
element: <Suspense><Settings /></Suspense> element: <Suspense><Settings /></Suspense>
}, },
{ {
path: 'workspaces', path: 'incidents',
element: <Suspense><Workspaces /></Suspense> element: <Suspense><Incidents /></Suspense>
}, },
{ {
path: 'scans', path: 'scans',

View File

@@ -6,9 +6,7 @@ export default function PublicLayout(): React.ReactElement {
return ( return (
<> <>
<PublicNavbar /> <PublicNavbar />
<main id="main-view"> <Outlet />
<Outlet />
</main>
<ToastContainer <ToastContainer
position='bottom-right' position='bottom-right'
autoClose={3000} autoClose={3000}

View File

@@ -7,18 +7,18 @@ import { FetchBaseQueryError } from "@reduxjs/toolkit/query"
import { SerializedError } from "@reduxjs/toolkit" import { SerializedError } from "@reduxjs/toolkit"
const MAX_DESCRIPTION_LENGTH = 79 const MAX_DESCRIPTION_LENGTH = 63
const MAX_LABEL_LENGTH = 32 const MAX_LABEL_LENGTH = 31
export function GraphLoaderCard() { export function GraphLoaderCard() {
return ( return (
<> <>
<div className="mb-2"> <div className="mb-2">
<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="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="space-y-3"> <div className="space-y-3">
<div className="h-2 w-3/5 rounded-lg bg-slate-600/20 animate-pulse"></div> <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-slate-600/20 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-slate-600/20 animate-pulse"></div> <div className="h-2 w-2/5 rounded-lg bg-mirage-500/60 animate-pulse"></div>
</div> </div>
</div> </div>
</div> </div>
@@ -54,16 +54,16 @@ export default function Subpanel({
const { hid } = useParams(); const { hid } = useParams();
return ( return (
<section className={styles["subpanel"]}> <div className={styles["subpanel"]}>
<header className={styles["subpanel-header"]} onClick={setShowEntities}> <div className={styles["subpanel-header"]} onClick={setShowEntities}>
<h2 className={`${styles["cyberpunk"]} ${styles["glitched"]}`}>{label ?? ""}</h2> <p>{label ?? ""}</p>
<ChevronDownIcon className={styles[`show-header-icon-${showEntities}`] + " transition-transform duration-100"} /> <ChevronDownIcon className={styles[`show-header-icon-${showEntities}`]} />
</header> </div>
{showError && showEntities && !isLoading && ( {showError && showEntities && !isLoading && (
<> <>
<p> <p>
{errorMessage?.length ? (<p className="text-slate-400/60 text-sm px-2">{errorMessage}</p>) : ( {errorMessage?.length ? (<>{errorMessage}</>) : (
<>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</> <>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</>
)} )}
</p> </p>
</> </>
@@ -74,33 +74,32 @@ export default function Subpanel({
<GraphLoaderCard /> <GraphLoaderCard />
</> </>
)} )}
{isSuccess && showEntities && (
<section className={` transition-transform duration-150 ease-out ${showEntities ? 'translate-y-0' : '-translate-y-[45%] -scale-y-0 !h-0'}`}> <section className={styles["subpanel-section"]}>
{items && items.map((item) => { {items && items.map((item) => {
const isActive = hid === item.id return (
const descriptionClassName = styles["subpanel-desc"] + " " + styles[`subpanel-desc-${isActive}`] <Link
return ( key={item.id}
<Link to={`${to}/${item.id}`}
key={item.id} className={styles["subpanel-link"] + " " + styles[`subpanel-link-${hid === item.id}`]}>
to={`${to}/${item.id}`} <div>
className={`${styles["subpanel-link"]} ${styles[`subpanel-link-${isActive}`]} ${isSuccess && showEntities ? 'translate-y-0 scale-y-100' : '-translate-y-full scale-y-0 '}`}> <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>
<div> <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-label"] + " " + styles[`subpanel-label-${isActive}`]}>{item.label}</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>
<p className={descriptionClassName}>{item.description}</p> </div>
<p className={descriptionClassName}><span className="font-sans">Last seen </span> {formatPGDate(item?.last_edited ? item.last_edited : item.last_seen)}</p> <StarIcon
</div> onClick={async () => await onClick(item.id)}
<StarIcon className={
onClick={async () => await onClick(item.id)} styles["link-icon"] + " " +
className={ styles[`link-icon-${item.is_favorite}`] + " " +
styles["link-icon"] + " " + styles[`link-active-${hid !== item.id}`]
styles[`link-icon-${item.is_favorite}`] + " " + }
styles[`link-active-${hid !== item.id}`] />
} </Link>
/> )
</Link> })}
) </section>
})} )}
</section> </div>
</section>
) )
} }

View File

@@ -92,9 +92,9 @@ export default function GraphDetails() {
return ( return (
<> <>
<section className="flex flex-col h-screen w-full"> <section className="flex flex-col h-screen w-full">
<header className="flex w-full"> <div className="flex w-full">
<GraphHeader refetchGraphs={async () => await refetchGraphs()} stats={graphStats} graph={activeGraph as Graph} /> <GraphHeader refetchGraphs={async () => await refetchGraphs()} stats={graphStats} graph={activeGraph as Graph} />
</header> </div>
<ResponsiveGridLayout <ResponsiveGridLayout
className="relative flex-grow h-full z-10 w-full" className="relative flex-grow h-full z-10 w-full"
rowHeight={1} rowHeight={1}

View File

@@ -18,7 +18,7 @@ export default function GraphHeader({ graph, stats, refetchGraphs }: GraphHeader
return ( return (
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className="flex w-full border-b-2 bg-mirage-800/25 border-mirage-400/20 relative"> <div className="flex w-full border-b-2 border-mirage-400/20 -top-[2px] relative">
<div className=" w-full mx-auto"> <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"> <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"> <div className="flex flex-col w-full py-1">
@@ -49,15 +49,15 @@ export default function GraphHeader({ graph, stats, refetchGraphs }: GraphHeader
</button> </button>
<Link <Link
to={`/graph/inquiry/${graph?.id}`} to={`/graph/inquiry/${graph?.id}`}
className='btn-primary mb-3.5 stroke-primary-300/90 hover:!stroke-primary-400/80' className='btn-primary mb-3.5'
> >
Open graph Open graph
<EyeIcon className='ml-2 w-5 h-5 stroke-inherit' /> <EyeIcon className='ml-2 w-5 h-5 ' />
</Link> </Link>
</div> </div>
</section> </section>
<hr className=" border-b border-mirage-400/20" /> <hr className=" border-b border-mirage-400/20" />
<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"> <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">
{graph && stats && ( {graph && stats && (
<> <>
<div className="flex items-center ml-[4px] gap-x-2"> <div className="flex items-center ml-[4px] gap-x-2">

View File

@@ -18,13 +18,13 @@ export default function GraphOverview() {
}, [graphsData]) }, [graphsData])
return <> return <>
<div className="w-full items-center justify-center my-auto relative -top-16"> <div className="w-full items-center justify-center my-14">
<div className="flex flex-col items-center justify-center text-slate-400"> <div className="grid place-items-center relative z-10 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 "> <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">
{favoriteGraphs.length === 0 && graphs.length === 0 && ( {favoriteGraphs.length === 0 && graphs.length === 0 && (
<> <>
<h1 className="text-slate-300/80 text-3xl lg:text-4xl font-bold pr-2">Oh no!</h1> <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="md:pt-4 max-w-xl text-slate-300/80"> <p className="py-6 md:py-8 max-w-lg">
We're usually a treasure chest of knowledge, We're usually a treasure chest of knowledge,
but we couldn't find any graphs. Get started but we couldn't find any graphs. Get started
by creating a new graph by creating a new graph
@@ -33,22 +33,15 @@ export default function GraphOverview() {
)} )}
{(graphs.length > 0 || favoriteGraphs.length > 0) && ( {(graphs.length > 0 || favoriteGraphs.length > 0) && (
<> <>
<h1 className="text-slate-300/80 text-3xl lg:text-4xl font-bold pr-2"> <h1 className="text-slate-300 text-3xl lg:text-4xl font-bold pt-12 sm:pt-14 lg:pt-8 ">
{graphs.length + favoriteGraphs.length ?? ''} {favoriteGraphs.length + graphs.length > 1 ? 'graphs' : 'graph'} available</h1> {graphs.length + favoriteGraphs.length ?? ''} {favoriteGraphs.length + graphs.length > 1 ? 'graphs' : 'graph'} available</h1>
<p className="md:pt-4 max-w-xl text-slate-300/80"> <p className="py-6 md:py-8 max-w-md">
Get started by selecting an existing graph from the sidebar Get started by selecting an existing graph from the sidebar
on the left or you can create a new graph with the button located towards the bottom left on the left, or try creating a new graph
</p> </p>
</> </>
)} )}
</div> </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>
</div> </div>
</> </>

View File

@@ -1,26 +1,23 @@
.subpanel section {
@apply py-1 flex flex-col h-min relative z-0 items-start justify-start
}
/* refactor me */
.subpanel-wrapper { .subpanel-wrapper {
@apply flex flex-col justify-between relative flex-shrink overflow-clip; @apply flex flex-col justify-between relative flex-shrink h-full pr-2;
} }
.subpanel > h2 { .subpanel-section {
@apply py-1 px-1 flex flex-col;
}
.subpanel > p {
@apply text-sm py-4 font-medium text-slate-500 focus:outline-none cursor-pointer px-2 @apply text-sm py-4 font-medium text-slate-500 focus:outline-none cursor-pointer px-2
} }
.subpanel-header { .subpanel-header {
@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 @apply flex pb-1 items-center hover:border-mirage-400/60 border-b transition-colors duration-100 ease-in-out border-transparent
} }
.subpanel-header h2 { .subpanel-header p {
@apply text-sm px-1 font-semibold font-display leading-3 select-none text-inherit focus:outline-none cursor-pointer @apply text-sm px-1 font-black leading-3 text-slate-600 focus:outline-none cursor-pointer
} }
.subpanel-header svg { .subpanel-header svg {
@apply h-5 w-5 mr-1 hover:rotate-3 origin-center text-slate-600 ml-auto transform focus:outline-none cursor-pointer @apply h-5 w-5 mr-2 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 { .show-header-icon-true {
@apply !-rotate-180 @apply !-rotate-180
} }
@@ -30,46 +27,36 @@
} }
.subpanel-link { .subpanel-link {
@apply mb-1 focus:outline-none bg-transparent border rounded-md border-transparent @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
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 { .subpanel-link div:hover .subpanel-label {
@apply !text-slate-400/95 @apply !text-slate-400/95
} }
.subpanel-link-true { .subpanel-link-true {
@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 @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
} }
.subpanel-link div { .subpanel-link div {
@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 @apply flex w-full p-3 flex-col items-start space-y-1.5 text-slate-400/30 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 { svg.link-icon {
@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 @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
} }
svg.link-icon-true { svg.link-icon-true {
@apply !text-primary-300 hover:!text-primary-200 !bg-transparent; @apply !text-primary-300 hover:!text-primary-200;
} }
svg.link-icon-false { svg.link-icon-false {
@apply text-slate-600 @apply text-slate-600
} }
.link-active-true { .link-active-true {
@apply !bg-transparent hover:!bg-slate-800/40 transition-colors duration-200 ; @apply !bg-transparent hover:!bg-slate-900 ;
} }
.subpanel-label { .subpanel-label {
@apply text-sm leading-4 font-display font-medium line-clamp-1 pb-0.5 @apply text-sm font-medium leading-none
} }
.subpanel-label-true { .subpanel-label-true {
@apply text-slate-400/95; @apply text-slate-400/95;
@@ -78,22 +65,8 @@ svg.link-icon-false {
@apply text-slate-500/60; @apply text-slate-500/60;
} }
.subpanel-desc { .subpanel-desc {
@apply text-xs font-display font-light leading-none text-left; @apply text-xs font-medium leading-none text-left;
} }
.subpanel-desc-true { .subpanel-desc-true {
@apply !text-slate-400/80 @apply !text-slate-400/60
} }
/* .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;
} */

View File

@@ -30,8 +30,8 @@ export default function EntitiesPanel({
const [updateEntityIsFavorite] = useUpdateEntityFavoriteIdMutation() const [updateEntityIsFavorite] = useUpdateEntityFavoriteIdMutation()
const updateEntityOnFavorite = (hid: string) => { const updateEntityOnBookmark = async (hid: string) => {
updateEntityIsFavorite({ hid }) await updateEntityIsFavorite({ hid })
refetchEntities() refetchEntities()
} }
@@ -45,7 +45,7 @@ export default function EntitiesPanel({
isLoading={isLoading} isLoading={isLoading}
isSuccess={isSuccess} isSuccess={isSuccess}
items={favoriteEntities} items={favoriteEntities}
onClick={(hid) => updateEntityOnFavorite(hid)} onClick={async (hid) => await updateEntityOnBookmark(hid)}
to="/dashboard/entity" to="/dashboard/entity"
/> />
<Subpanel <Subpanel
@@ -56,7 +56,7 @@ export default function EntitiesPanel({
isLoading={isLoading} isLoading={isLoading}
isSuccess={isSuccess} isSuccess={isSuccess}
items={entities} items={entities}
onClick={(hid) => updateEntityOnFavorite(hid)} onClick={async (hid) => await updateEntityOnBookmark(hid)}
to="/dashboard/entity" to="/dashboard/entity"
/> />
</section > </section >

View File

@@ -1,57 +1,51 @@
/* wraps the sidebar with search and the main view */ /* wraps the sidebar with search and the main view */
.sidebar-wrapper { .sidebar-wrapper {
@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 @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
} }
.search-container { .search-container {
@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 @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
} }
.search-container:active, .search-container:focus-within { .search-container:active, .search-container:focus-within {
@apply border-primary/50 to-mirage-400/10 from-mirage-400/20 bg-gradient-to-l @apply border-primary/50 to-mirage-400/10 from-mirage-400/20 bg-gradient-to-l
} }
.search-container input { .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 { .search-container svg {
@apply w-5 h-5 text-inherit ml-2 @apply w-5 h-5 text-slate-700 ml-2
} }
.dashboard-tabs { .dashboard-tabs {
@apply overflow-y-hidden flex-grow flex flex-col items-stretch @apply overflow-y-hidden flex-grow h-full pb-12
} }
.tabs-list { .tabs-list {
@apply flex justify-between rounded pb-2 pt-0.5 items-center; @apply flex justify-between w-full rounded p-1 ;
} }
.tab { .tab {
@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 @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
}
.tab span,
.tab-false span,
.tab-true span {
@apply min-w-min
} }
.tab-true { .tab-true {
@apply text-slate-300/90 ; @apply text-slate-300 ;
} }
.tab-false { .tab-false {
@apply text-slate-600 hover:text-slate-500 ; @apply text-slate-600 hover:text-slate-500;
} }
.tab-slider { .tab-slider {
@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; @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
/* clip-path: polygon(0px 0px, 0px 26px, 9px 36px, 90px 36px, 90px 0px, 0px 0px, 0px 0px ); */
} }
.graph-tab[aria-selected="true"] ~ div { .graph-tab[aria-selected="true"] ~ div {
@apply -translate-x-[300px] scale-x-[104%] cursor-pointer @apply translate-x-[0px] w-[98px]
} }
.entities-tab[aria-selected="true"] ~ div { .entities-tab[aria-selected="true"] ~ div {
@apply -translate-x-[200px] scale-x-[110%] cursor-pointer @apply translate-x-[101px] w-[100px]
} }
.market-tab[aria-selected="true"] ~ div { .market-tab[aria-selected="true"] ~ div {
@apply -translate-x-[95px] scale-x-105 cursor-pointer @apply translate-x-[205px] w-[93px]
} }
.tab-panel { .tab-panel {
@apply w-full relative px-2 pr-2.5; @apply w-full relative;
} }

View File

@@ -1,6 +1,6 @@
import { Fragment, useEffect, useRef, useState } from "react"; import { Fragment, useEffect, useRef, useState } from "react";
import { Link, Outlet, useLocation, useParams } from "react-router-dom"; import { Link, Outlet, useLocation, useParams } from "react-router-dom";
import { MagnifyingGlassIcon, PlusIcon, CloudIcon } from "@heroicons/react/24/outline"; import { MagnifyingGlassIcon, PlusIcon } from "@heroicons/react/24/outline";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
import GraphPanel from "./_components/tabs/GraphPanel"; import GraphPanel from "./_components/tabs/GraphPanel";
import EntitiesPanel from "./_components/tabs/EntitiesPanel"; import EntitiesPanel from "./_components/tabs/EntitiesPanel";
@@ -37,7 +37,7 @@ export default function DashboardPage() {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const location = useLocation() const location = useLocation()
const initialTab = location.pathname.includes("entity") ? const initialTab = location.pathname.includes("entity") ?
0 : location.pathname.includes("market") 1 : location.pathname.includes("market")
? 2 : 0 ? 2 : 0
const [tabIndex, setTabIndex] = useState<number>(initialTab) const [tabIndex, setTabIndex] = useState<number>(initialTab)
@@ -104,13 +104,12 @@ export default function DashboardPage() {
onChange={setTabIndex} onChange={setTabIndex}
className={styles["dashboard-tabs"]} className={styles["dashboard-tabs"]}
> >
<Tab.List className={`${styles["tabs-list"]}`}> <Tab.List className={styles["tabs-list"]}>
<Tab as={Fragment}> <Tab as={Fragment}>
{({ selected }) => ( {({ selected }) => (
<Link <Link
to='graph' to='graph'
className={`${styles["tab"]} ${styles["graph-tab"]} ${styles[`tab-${selected}`]}`} className={`${styles["tab"]} ${styles["graph-tab"]} ${styles[`tab-${selected}`]}`}
aria-selected={selected}
> >
Graphs Graphs
</Link> </Link>
@@ -121,11 +120,8 @@ export default function DashboardPage() {
<Link <Link
to='entity' to='entity'
className={`${styles["tab"]} ${styles["entities-tab"]} ${styles[`tab-${selected}`]}`} className={`${styles["tab"]} ${styles["entities-tab"]} ${styles[`tab-${selected}`]}`}
aria-selected={selected}
> >
<span> Entities
Entities
</span>
</Link> </Link>
)} )}
</Tab> </Tab>
@@ -134,15 +130,14 @@ export default function DashboardPage() {
<Link <Link
to='market' to='market'
className={`${styles["tab"]} ${styles["market-tab"]} ${styles[`tab-${selected}`]}`} className={`${styles["tab"]} ${styles["market-tab"]} ${styles[`tab-${selected}`]}`}
aria-selected={selected}
> >
Market Market
</Link> </Link>
)} )}
</Tab> </Tab>
<div className={styles["tab-slider"]} /> <div className={styles["tab-slider"]}></div>
</Tab.List> </Tab.List>
<div className="h-full overflow-y-scroll "> <section className="h-full overflow-y-scroll mb-20">
<Tab.Panel className={styles["tab-panel"]}> <Tab.Panel className={styles["tab-panel"]}>
<GraphPanel <GraphPanel
refetchGraphs={async () => await refetchGraphs()} refetchGraphs={async () => await refetchGraphs()}
@@ -165,7 +160,7 @@ export default function DashboardPage() {
<Tab.Panel className={styles["tab-panel"]}> <Tab.Panel className={styles["tab-panel"]}>
<MarketPanel /> <MarketPanel />
</Tab.Panel> </Tab.Panel>
</div> </section>
</Tab.Group> </Tab.Group>
{tabIndex !== 2 ? ( {tabIndex !== 2 ? (
<button <button
@@ -176,14 +171,13 @@ export default function DashboardPage() {
className='btn-primary mt-auto mb-4 mx-4 mr-6' className='btn-primary mt-auto mb-4 mx-4 mr-6'
> >
Create {tabIndex === 0 ? 'graph' : 'entity'} Create {tabIndex === 0 ? 'graph' : 'entity'}
<PlusIcon className="!ml-7" /> <PlusIcon />
</button> </button>
) : ( ) : (
<button <button
className='btn-primary mx-4 mr-6 mt-auto mb-4' className='btn-primary mx-4 mr-6'
> >Add transforms
Connect server plugins
<CloudIcon />
</button> </button>
)} )}
</aside> </aside>

View File

@@ -125,7 +125,7 @@ export default function EditEntityNode({ ctx, sendJsonMessage, closeRef }: JSONO
/> />
); );
case 'empty': case 'empty':
return <input className='h-0 bg-transparent pointer-events-none' />; return <div className='hidden' />;
} }
}; };

View File

@@ -4,10 +4,10 @@ import {
TableCellsIcon, TableCellsIcon,
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { useState, useEffect, useCallback, useMemo, useLayoutEffect } from 'react'; import { useState, useEffect, useCallback, useMemo, useLayoutEffect } from 'react';
import { Link, useLocation, useParams } from 'react-router-dom'; import { Link } from 'react-router-dom';
import 'react-grid-layout/css/styles.css'; import 'react-grid-layout/css/styles.css';
import { Responsive, WidthProvider, Layout } from 'react-grid-layout'; import { Responsive, WidthProvider, Layout } from 'react-grid-layout';
import { useGetEntitiesQuery, useRefreshEntityPluginsQuery } from '@src/app/api'; import { useGetEntitiesQuery } from '@src/app/api';
import { selectPositionMode, selectViewMode, setAllEdges, setAllNodes, setEditState, setNodeType, setPositionMode, setViewMode } from '@src/features/graph/graphSlice'; import { selectPositionMode, selectViewMode, setAllEdges, setAllNodes, setEditState, setNodeType, setPositionMode, setViewMode } from '@src/features/graph/graphSlice';
import classNames from 'classnames'; import classNames from 'classnames';
import { useAppDispatch, } from '@src/app/hooks'; import { useAppDispatch, } from '@src/app/hooks';
@@ -64,11 +64,11 @@ export const useResize = ({ minWidth }: UseResizeProps): UseResizeReturn => {
export function EntityOption({ entity, onDragStart }: JSONObject) { export function EntityOption({ entity, onDragStart }: JSONObject) {
return ( return (
<> <>
<li key={entity.id} className='flex items-center w-full justify-between pb-2.5'> <li key={entity.id} className='flex items-center w-full justify-between py-3'>
<div <div
draggable draggable
onDragStart={(event) => onDragStart(event, entity.label)} onDragStart={(event) => onDragStart(event, entity.label)}
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' 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'
> >
<div className='flex flex-col w-full select-none'> <div className='flex flex-col w-full select-none'>
<div className='flex items-start justify-between gap-x-3 w-full relative'> <div className='flex items-start justify-between gap-x-3 w-full relative'>
@@ -83,15 +83,16 @@ export function EntityOption({ entity, onDragStart }: JSONObject) {
{entity.status}Installed {entity.status}Installed
</p> */} </p> */}
</div> </div>
<div className='flex flex-wrap items-center gap-x-2 text-xs leading-5 text-slate-500'> <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 line-clamp-2 text-slate-500'> <p className='truncate whitespace-normal leading-5 text-slate-500'>
{entity.description} {' '}
{entity.description && entity.description.length > 76 ?
`${entity.description.slice(0, 76)}...` : entity.description}
</p> </p>
<br /> <svg viewBox='0 0 2 2' className='h-0.5 w-0.5 fill-current'>
<p className='truncate flex items-center leading-5 text-slate-500 text-xs'> <circle cx={1} cy={1} r={1} />
<svg viewBox='0 0 2 2' className='h-0.5 w-0.5 fill-current mr-1.5 ml-0'> </svg>
<circle cx={1} cy={1} r={1} /> <p className='truncate leading-5 text-slate-500 text-xs'>
</svg>
Created by {entity.author ? entity.author : 'the OSINTBuddy team'} Created by {entity.author ? entity.author : 'the OSINTBuddy team'}
</p> </p>
</div> </div>
@@ -107,20 +108,19 @@ const ResponsiveGridLayout = WidthProvider(Responsive);
const MAX_GRAPH_LABEL_LENGTH = 22; const MAX_GRAPH_LABEL_LENGTH = 22;
export default function EntityOptions({ positionMode, activeGraph, setElkLayout, toggleForceLayout, fitView }: JSONObject) { export default function EntityOptions({ positionMode, activeGraph, setElkLayout, toggleForceLayout, fitView }: JSONObject) {
const { hid = "" } = useParams()
const { const {
data: entitiesData = { plugins: [], }, data: entitiesData = { entities: [], count: 0, favorite_entities: [], favorite_count: 0 },
isLoading, isLoading,
isSuccess, isSuccess,
isError isError
} = useRefreshEntityPluginsQuery({ hid }) } = useGetEntitiesQuery({ skip: 0, limit: 50 })
const [showEntities, setShowEntities] = useState(true); const [showEntities, setShowEntities] = useState(true);
const [searchFilter, setSearchFilter] = useState(''); const [searchFilter, setSearchFilter] = useState('');
const entities = useMemo(() => searchFilter const entities = useMemo(() => searchFilter
? [...entitiesData?.plugins.filter((entity: JSONObject) => entity.label.toLowerCase().includes(searchFilter.toLowerCase()))] ? [...entitiesData?.entities.filter((entity: JSONObject) => entity.label.toLowerCase().includes(searchFilter.toLowerCase())),
: [...entitiesData?.plugins], [searchFilter, entitiesData]) ...entitiesData?.favorite_entities.filter((entity: JSONObject) => entity.label.toLowerCase().includes(searchFilter.toLowerCase()))]
: [...entitiesData?.entities, ...entitiesData?.favorite_entities], [searchFilter, entitiesData])
const onDragStart = (event: DragEvent, nodeType: string) => { const onDragStart = (event: DragEvent, nodeType: string) => {
if (event?.dataTransfer) { if (event?.dataTransfer) {
@@ -195,7 +195,7 @@ export default function EntityOptions({ positionMode, activeGraph, setElkLayout,
}} }}
> >
<div <div
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' 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'
key='entities' key='entities'
id='node-options-tour' id='node-options-tour'
> >
@@ -228,7 +228,7 @@ export default function EntityOptions({ positionMode, activeGraph, setElkLayout,
</ol> </ol>
{showEntities && ( {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/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'> <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'>
<input <input
onChange={(e) => setSearchFilter(e.target.value)} onChange={(e) => setSearchFilter(e.target.value)}
className='block w-full placeholder:text-slate-700 bg-transparent outline-none sm:text-sm' 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>
<div <div
key='positions' key='positions'
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'> 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'>
<ol className='text-sm flex select-none relative px-4 pt-1'> <ol className='text-sm flex select-none relative px-4 pt-1'>
<li className='flex mr-auto'> <li className='flex mr-auto'>
<h5 <h5
@@ -284,7 +284,7 @@ export default function EntityOptions({ positionMode, activeGraph, setElkLayout,
type='button' type='button'
className={classNames( 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', '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/80 hover:bg-mirage-800 border-primary-400/50 hover:border-primary-400/50 ' positionMode === 'manual' && 'bg-mirage-800 hover:bg-mirage-800 border-primary-400/50 hover:border-primary-400/50 '
)} )}
> >
<HandRaisedIcon <HandRaisedIcon
@@ -301,7 +301,7 @@ export default function EntityOptions({ positionMode, activeGraph, setElkLayout,
type='button' type='button'
className={classNames( 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', '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/80 hover:bg-mirage-800 border-primary-400/50 hover:border-primary-400/50 ' positionMode === 'force' && 'bg-mirage-800 hover:bg-mirage-800 border-primary-400/50 hover:border-primary-400/50 '
)} )}
> >
<Icon <Icon
@@ -326,7 +326,7 @@ export default function EntityOptions({ positionMode, activeGraph, setElkLayout,
type='button' type='button'
className={classNames( 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', '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/80 hover:bg-mirage-800 border-primary-400/50 hover:border-primary-400/50 ' positionMode === 'right tree' && 'bg-mirage-800 hover:bg-mirage-800 border-primary-400/50 hover:border-primary-400/50 '
)} )}
> >
<Icon <Icon
@@ -348,7 +348,7 @@ export default function EntityOptions({ positionMode, activeGraph, setElkLayout,
type='button' type='button'
className={classNames( 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', '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/80 hover:bg-mirage-800 border-primary-400/50 hover:border-primary-400/50 ' positionMode === 'tree' && 'bg-mirage-800 hover:bg-mirage-800 border-primary-400/50 hover:border-primary-400/50 '
)} )}
> >
<Icon <Icon

View File

@@ -1,74 +0,0 @@
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;

View File

@@ -3,7 +3,7 @@ import { ChevronUpDownIcon, PaperClipIcon, XMarkIcon } from '@heroicons/react/24
import { Combobox } from '@headlessui/react'; import { Combobox } from '@headlessui/react';
import classNames from 'classnames'; import classNames from 'classnames';
import { ChangeEvent, Dispatch, Fragment, useEffect, useMemo, useRef, useState } from 'react'; import { ChangeEvent, Dispatch, Fragment, useEffect, useMemo, useRef, useState } from 'react';
import { Handle, HandleType, Position } from 'reactflow'; import { Handle, Position } from 'reactflow';
import { GripIcon, Icon } from '@src/components/Icons'; import { GripIcon, Icon } from '@src/components/Icons';
import { Dialog } from '@headlessui/react'; import { Dialog } from '@headlessui/react';
import { useAppDispatch, useAppSelector, useComponentVisible } from '@src/app/hooks'; import { useAppDispatch, useAppSelector, useComponentVisible } from '@src/app/hooks';
@@ -19,6 +19,20 @@ import {
} from '@src/features/graph/graphSlice'; } from '@src/features/graph/graphSlice';
import { toast } from 'react-toastify'; 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 }; const handleStyle = { borderColor: '#39477899', background: '#12172720', width: 12, margin: -1, height: 12 };
type NodeElement = NodeInput & { type NodeElement = NodeInput & {
@@ -29,10 +43,13 @@ type NodeElement = NodeInput & {
}; };
export default function ViewEntityNode({ ctx }: JSONObject) { export default function ViewEntityNode({
ctx,
dispatch
}: JSONObject) {
const node = ctx.data; const node = ctx.data;
const displayValue = useMemo(() => Array.isArray(node.elements[0]) ? node.elements[0][0]?.value : node.elements[0]?.value, [node.elements])
const displayValue = useMemo(() => Array.isArray(node.elements[0]) ? node.elements[0][0]?.value : node.elements[0]?.value, [node.elements])
return ( return (
<> <>
<Handle position={Position.Right} id='r1' key='r1' type='source' style={handleStyle} /> <Handle position={Position.Right} id='r1' key='r1' type='source' style={handleStyle} />
@@ -50,7 +67,7 @@ export default function ViewEntityNode({ ctx }: JSONObject) {
style={{ backgroundColor: node?.color?.length === 7 ? `${node.color}99` : node?.color }} style={{ backgroundColor: node?.color?.length === 7 ? `${node.color}99` : node?.color }}
className='header !rounded-full !p-3' className='header !rounded-full !p-3'
> >
<Icon icon={node.icon} className='!h-14 !w-14 text-slate-300/95 cursor-grab focus:cursor-grabbing' /> <Icon icon={node.icon} className='!h-20 !w-20 cursor-grab focus:cursor-grabbing' />
</div> </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`}> <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} {displayValue?.length >= 90 ? `${displayValue.slice(0, 90)}...` : displayValue}

View File

@@ -7,19 +7,13 @@ import ReactFlow, {
NodeDragHandler, NodeDragHandler,
Connection, Connection,
Node, Node,
getViewportForBounds,
MiniMap,
ReactFlowInstance,
getNodesBounds,
getBoundsOfRects,
getTransformForBounds,
} from 'reactflow'; } from 'reactflow';
import EditEntityNode from './EntityEditNode'; import EditEntityNode from '../_components/EditEntityNode';
import { addNodeUpdate, createEdge, disableEntityEdit, enableEntityEdit, graph, onEdgesChange, selectEditState, setEditLabel, setEditState, updateEdgeEvent, updateNodeFlow } from '@src/features/graph/graphSlice'; import { addNodeUpdate, createEdge, disableEntityEdit, enableEntityEdit, onEdgesChange, selectEditState, setEditLabel, setEditState, updateEdgeEvent, updateNodeFlow } from '@src/features/graph/graphSlice';
import { useAppDispatch, useAppSelector, useEffectOnce } from '@src/app/hooks'; import { useAppDispatch, useAppSelector } from '@src/app/hooks';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import ViewEntityNode from './EntityViewNode'; import ViewEntityNode from '../_components/ViewEntityNode';
import { CreateEntityOnDropApiResponse, useCreateEntityOnDropMutation, useRefreshEntityPluginsQuery } from '@src/app/api'; import { CreateEntityOnDropApiResponse, useCreateEntityOnDropMutation, useRefreshPluginsQuery } from '@src/app/api';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import NewConnectionLine from './ConnectionLine'; import NewConnectionLine from './ConnectionLine';
import SimpleFloatingEdge from './SimpleFloatingEdge'; import SimpleFloatingEdge from './SimpleFloatingEdge';
@@ -31,7 +25,6 @@ const viewOptions: FitViewOptions = {
// im lazy so im extending the generic JSONObject for now, feel free to fix... // im lazy so im extending the generic JSONObject for now, feel free to fix...
interface ProjectGraphProps extends JSONObject { interface ProjectGraphProps extends JSONObject {
graphInstance?: ReactFlowInstance
} }
export default function Graph({ export default function Graph({
@@ -55,7 +48,7 @@ export default function Graph({
[] []
); );
const { hid } = useParams() const { hid } = useParams()
useRefreshEntityPluginsQuery({ hid: hid as string }) useRefreshPluginsQuery({ hid: hid as string })
const onDragOver: DragEventHandler<HTMLDivElement> = useCallback((event) => { const onDragOver: DragEventHandler<HTMLDivElement> = useCallback((event) => {
event.preventDefault(); event.preventDefault();
@@ -75,7 +68,7 @@ export default function Graph({
if (typeof label === 'undefined' || !label) return; if (typeof label === 'undefined' || !label) return;
const graphBounds = graphRef.current.getBoundingClientRect(); const graphBounds = graphRef.current.getBoundingClientRect();
const position = graphInstance?.project({ const position = graphInstance.project({
x: event.clientX - graphBounds.left, x: event.clientX - graphBounds.left,
y: event.clientY - graphBounds.top, y: event.clientY - graphBounds.top,
}); });
@@ -121,7 +114,7 @@ export default function Graph({
[] []
); );
const doubleClickThreshold = 320; const doubleClickThreshold = 325;
const [isDragging, setIsDragging] = useState(false) const [isDragging, setIsDragging] = useState(false)
const [isDoubleClick, setIsDoubleClick] = useState(false) const [isDoubleClick, setIsDoubleClick] = useState(false)
const [entityPosition, setEntityPosition] = useState<any>({ x: null, y: null }) const [entityPosition, setEntityPosition] = useState<any>({ x: null, y: null })
@@ -137,31 +130,9 @@ 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 ( return (
<ReactFlow <ReactFlow
onlyRenderVisibleElements={true} minZoom={0.2}
nodeDragThreshold={20}
minZoom={0.22}
maxZoom={2.0} maxZoom={2.0}
nodes={nodes} nodes={nodes}
edges={edges} edges={edges}
@@ -183,12 +154,8 @@ export default function Graph({
setClickDelta(newDelta) setClickDelta(newDelta)
setIsDoubleClick(isDouble) setIsDoubleClick(isDouble)
}} }}
onMoveStart={() => !isDragging && setIsDragging(true)}
onMoveEnd={() => setIsDragging(false)}
fitViewOptions={viewOptions} fitViewOptions={viewOptions}
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
onDragStart={() => setIsDragging(true)}
onDragEnd={() => setIsDragging(false)}
panActivationKeyCode='Space' panActivationKeyCode='Space'
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
onPaneClick={onPaneClick} onPaneClick={onPaneClick}
@@ -197,8 +164,9 @@ export default function Graph({
onSelectionContextMenu={onMultiSelectionCtxMenu} onSelectionContextMenu={onMultiSelectionCtxMenu}
connectionLineComponent={NewConnectionLine} connectionLineComponent={NewConnectionLine}
elevateNodesOnSelect={true} elevateNodesOnSelect={true}
fitView
> >
<Background size={2.5} variant={BackgroundVariant.Dots} className='bg-transparent' color='#334155e6' /> <Background size={2} variant={BackgroundVariant.Dots} className='bg-transparent' color='#35426FAA' />
</ReactFlow> </ReactFlow>
); );
} }

View File

@@ -0,0 +1,37 @@
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;

Some files were not shown because too many files have changed in this diff Show More