Extension File Structure
Extensions are split into three main parts: Frontend, Backend, and Database.
All Extensions have a package name, which is defined in the backend Cargo.toml file, and is also required in the Metadata.toml file. These are semi-java-like package names, so they should be all lowercase, and can contain dots, for example dev.0x7d8.test.
Package name with underscores (also referred to as package identifiers) means dots are replaced with underscores, so dev.0x7d8.test turns into dev_0x7d8_test.
Initializing an Extension
Use the extension templates to get an extension up and running quickly.
bash
# create a new extension from the template, replace the name with your package name with underscores
panel-rs extensions init dev.0x7d8.test # <-- replace this with your package nameFrontend
bash
backend-extensions/
(package_identifier)/
frontend/
package.json # REQUIRED file containing additional dependencies
public/ # optional directory to include static files,
file1.jpg # this file would be available at <url>/file1.jpg
src/ # REQUIRED directory for typescript src
app.css # optional css file, bundled as its own chunk so it can be disabled with the extension
index.(ts|tsx) # REQUIRED file containing extension entrypoint
translations.ts # optional file containing extension translationsCompatibility symlinks
Everything an extension owns lives under backend-extensions/<package_identifier>/. The older paths frontend/extensions/<identifier> and database/extension-migrations/<identifier> still exist as symlinks so existing tooling keeps working, and which of the pair is the real directory depends on the container type. Treat backend-extensions/ as canonical: deleting or copying "the directory" through the legacy path may only move a link.
package.json
Add dependencies for your frontend extension code to this package.json, or leave it as-is - the required extension code and all dependencies of the base panel are already available.
json
{
"name": "extension",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
"dependencies": {
"shared": "workspace:*"
}
}src/index.ts
ts
import { Extension, ExtensionContext } from 'shared';
import type { MantineThemeOverride } from '@mantine/core';
// the class name doesn't matter much, but naming it after your package is advisable
class Dev0x7d8TestExtension extends Extension {
public cardConfigurationPage: React.FC | null = null;
public cardComponent: React.FC | null = null;
public cardIcon: React.ReactNode = null;
// Your extension entrypoint, this runs when the page is loaded
public initialize(ctx: ExtensionContext): void {
console.log('Dev0x7d8TestExtension initialized!', ctx);
}
// Your extension mantine theme entrypoint, this runs when the page is loaded
public initializeMantineTheme(ctx: ExtensionContext): MantineThemeOverride {
return {};
}
// Your extension can also provide a resolver for css variables, this runs when the page is loaded
public initializeMantineCssResolver(ctx: ExtensionContext): CSSVariablesResolver | null {
return null;
}
/**
* Your extension call processor, this can be called by other extensions to interact with yours,
* if the call does not apply to your extension, simply return `ctx.skip()` to continue the matching process.
*
* Optimally (if applies) make sure your calls are globally unique, for example by prepending them with `yourauthorname_yourextensioname_`
*/
public processCall(ctx: ExtensionContext, name: string, args: object): unknown {
return ctx.skip();
}
// https://typedocs.calagopus.com/classes/extensions_shared_src_extension.Extension
}
export default new Dev0x7d8TestExtension();Backend
bash
backend-extensions/
(package_name_with_underscores)/
Cargo.toml # REQUIRED file containing extension identifier (again), Author information and dependencies
Metadata.toml # REQUIRED file containing additional extension information
src/ # REQUIRED directory for backend rust src
lib.rs # REQUIRED file containing extension backend entrypointCargo.toml
toml
[package]
name = "dev_0x7d8_test" # once again, package name with underscores
description = "Test John Pork effortlessly." # short description of your extension
authors = ["0x7d8"] # authors of your extension
version = "1.0.0" # version of your extension
edition = { workspace = true }
[dependencies]
shared = { workspace = true }
async-trait = { workspace = true }
tracing = { workspace = true }Metadata.toml
toml
package_name = "dev.0x7d8.test" # package name without underscores
name = "0x7d8 Extension Test" # human-readable name of your extension
panel_version = ">=1.1.0" # panel version requirement of your extension, must be a valid semver comparatorWARNING
panel_version is enforced when the extension is loaded. The requirement must rule out panel versions older than 1.1.0 - a requirement like >=1.0.0 (which would admit pre-1.1.0 panels) is declined outright, and an extension whose requirement doesn't match the running panel version won't load either.
src/lib.rs
rs
use shared::{State, extensions::Extension};
#[derive(Default)]
pub struct ExtensionStruct; // must be named this, must implement Default and Send
#[async_trait::async_trait]
impl Extension for ExtensionStruct {
async fn initialize(&mut self, _state: State) {
tracing::info!("dev_0x7d8_test extension initialize called");
}
// https://cratedocs.calagopus.com/shared/extensions/trait.Extension
}Database (optional)
bash
backend-extensions/
(package_identifier)/
migrations/
(yyyymmddhhmmss)_migration_name/
up.sql # REQUIRED file containing the SQL statements to apply the migration
down.sql # REQUIRED file containing the SQL statements to rollback the migrationINFO
Auto-generate the migration files by running panel-rs database-migrator create <package_name> - this creates a new migration with the correct timestamp and file structure for you to fill in.
up.sql
sql
-- SQL statements to apply the migration, for example:
CREATE TABLE IF NOT EXISTS dev_0x7d8_test_table (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);down.sql
sql
-- SQL statements to rollback the migration, for example:
DROP TABLE IF EXISTS dev_0x7d8_test_table;